feat(autonudge): optional per-loop banner for the visible nudge row - #7777
Conversation
|
👋 Hi! This PR is currently in draft status. Workflow runs won't be auto-approved until it's marked as ready for review. When you're ready, click "Ready for review" and the workflows will be approved on the next cycle automatically. |
1 similar comment
|
👋 Hi! This PR is currently in draft status. Workflow runs won't be auto-approved until it's marked as ready for review. When you're ready, click "Ready for review" and the workflows will be approved on the next cycle automatically. |
967bf4b to
5bba485
Compare
GPT 5.6 Review (fork) — ✅ no blocking findingsReviewed Review detailsNo findings. |
Design Review (Fable 5, fork) — ✅ PASSDesign-level review of Design-Verdict: PASS A measured, quantified harm fixed at its root cause — one string, two consumers — via a purely additive, opt-in field with both upgrade and downgrade round-trips proven. [DESIGN-REVIEWED] a93297d |
First Principles Review (Fable 5, fork) — ✅ PASSPremise-level review of All checks are done. The one candidate finding I chased — that a banner supplied alongside a structured monitor would skip normalization and persist raw — died on verification: in First-Principles-Verdict: PASS Every item traces to one measured cost — 44 nudge rows at ~7.9KB were 51.8% of a 671,900-char session file — or to a named trust boundary, and the riders are declared. What this change shipsIntent: stop a long-running nudge loop from filling its own session file with per-cycle copies of its multi-KB instruction, without shortening what the model receives — an ADDITION.
Verification notes: the earlier [FIRST-PRINCIPLES-REVIEWED] a93297d |
Opus 4.8 Review (fork) — ✅ no blocking findingsReviewed Review detailsThe candidate hinges on whether a gated
So No further grounded defect surfaced in the diff. No findings. [OPUS-REVIEWED] a93297d |
5bba485 to
230c8d4
Compare
230c8d4 to
28a85ac
Compare
0a019f9 to
7419359
Compare
7419359 to
8e65c6b
Compare
8e65c6b to
1210fbd
Compare
1210fbd to
9ad38ea
Compare
9ad38ea to
17daa34
Compare
Audit note — #5999 is being closed in favour of this PRYou are the surviving re-cut; #5999 is being closed. What the two sharedBoth are OPEN, both are exactly ONE commit off the SAME merge-base d402acd (no stacking), and origin/main carries no banner at all: no What #5999 had that this PR does notPlease pick these up (or say they are not wanted) so they do not disappear with that branch: Re-cut as the promised split-b BEFORE closing 5999; none of it exists anywhere else. (1) src/kiro_crew/dashboard/handlers/autonudge.py::_serialize -- the denylist egress scrub (_UNSCRUBBED_FIELDS = ADDRESSING_FIELDS, per-field scrub_loop_text, From a repository-wide duplicate/overlap audit of every pull request open against |
Audit note — #7996 rehomes the work #5999 was closed for, and collides with this PR#7996 (same author) carries the egress-scrub work that #5999 was closed for — so nothing was lost in that closure. It also collides with this PR in seven files, and one collision is semantic rather than textual: Verification that the harvest is genuinely coveredRead the merge-base diff of #7996 (5,649 lines) end to end, its body.md, #5999's closing comment, #7777's diff, and checked every item against ITEM BY ITEM — all eight covered by #7996:
DISJOINTNESS PROVEN BOTH WAYS: zero harvest tokens in #7777's current head (all 12 probes = 0); zero banner tokens in #7996 (
From a repository-wide duplicate/overlap audit of every pull request open against |
A loop may opt into a short `banner` shown in the dashboard transcript row while the model still receives the full `message` every cycle. Default-off: an empty banner leaves the row byte-identical, and a persisted banner is repaired (non-string/oversized blanked) and capped at 500 chars.
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
bolichen97
left a comment
There was a problem hiding this comment.
The split between the visible transcript row and the model prompt is done in the one place that matters -- _fire_dashboard_nudge appends visible while _run_chat still receives the full tagged string -- so the per-cycle store/broadcast bloat is cured without ever shortening the instruction the nudge exists to re-deliver. Default-off is real (blank banner leaves content and meta.nudge byte-identical), the cap+redaction sequence is centralized in normalize_banner and re-checked after masking because redaction can grow the string, _load repairs a non-string/oversized/credential-bearing persisted banner so a hand-edited store cannot AttributeError the fire path, and the channel-bound refusal prevents storing config the Slack/Discord/Webex fire paths could never honour.
Problem / Motivation
An auto-nudge loop's
messageserves two consumers with opposite needs. The model needs thewhole instruction re-delivered every cycle — that is the guarantee the nudge exists to provide.
The dashboard already collapses each nudge to a one-line chip (expandable to the full body), so a
reader is not drowned on screen — but the full multi-KB
messageis still STORED and BROADCAST asa transcript row every cycle: measured on one long-running loop, 44 nudge rows of ~7.9KB were
51.8% of the entire 671,900-char session file.
Why it matters
The stored/broadcast bloat is per-cycle and unbounded, so a long babysit loop fills its own
session file with repeated copies of the prompt. The chip hides that payload from the reader on
screen, but nothing today keeps the model's full instruction while storing and broadcasting only a
short line. The tradeoff of the fix is explicit: on a bannered row the card's expansion shows the
banner rather than the instruction, and because ordinary loops are remove-on-stop the full
per-cycle instruction leaves the transcript once the loop ends.
What changed
Adds an optional per-loop
banner: str = ""— a SHORT stand-in shown in the visible dashboardtranscript row while the prompt still carries the full
message.bannerfield onNudgeLoop, threaded throughadd/updateand persisted;_loadrepairs a hand-edited store (a non-string or oversized banner is blanked, and apersisted credential-shaped banner is scrubbed).
_loadalso credential-scrubs the persistedmessagewith the same two passes — a security-positive hardening of the sibling field, sincethe store is writable out-of-band and served raw by
GET /api/autonudge; the authorized writepaths already scrub
message, so this closes the load path. Unlike the banner,messageisredaction-only (never blanked on length): it is the payload the model receives and has no
fallback row.
_fire_dashboard_nudgebuilds the visible row from the banner when set, and theprompt handed to
_run_chatis never shortened. The row'smeta.nudgecarries onlycycleand
loop_idand stays byte-identical whether or not a banner shows — the banner distinguishesthe row through its visible content, and the banner TEXT is never copied into meta. Channel-bound
loops (
slack:/discord:/webex:) have no separate display surface, so a banner there isrefused with 400.
POST/PATCH /api/autonudgeand the MCPmonitor_start/monitor_updatetoolsaccept
banner(capped atMAX_BANNER_CHARS= 500);PATCHcan quiet a running loop withoutresetting its budgets, and an explicit
""clears it. The/goalcommand sets one from theobjective.
passes the sibling
messagefield already gets in both authorizers (redact_exfiltration_urlsthen
redact_credentials), and the cap is re-checked after redaction because masking can growthe string. A banner is persisted and served by
GET /api/autonudge, so a short field is not asafe place to park a credential.
AutoNudgeService.get_by_idaccessor replaces a second inline id-scan; itstwo consumers are
autonudge_authz.py(the update-path channel refusal) anddashboard/handlers/autonudge.py(the DELETE handler).Default-off: with no banner, every existing loop's row — content and
meta.nudge— isbyte-identical to before, and the store round-trips on a build that predates the field.
Scope: only
/goalsets a banner automatically; otherwise a row is shortened only when aproducer (a REST or MCP caller) supplies a
banner, so the per-cycle bloat is cured for a loopexactly when it opts in. A server-side default that banners every loop is deliberately out of
scope for this PR and left to a follow-up.
Tests
test/test_autonudge_banner.py(default byte-identity, row-vs-prompt divergence proven at theprompt, non-string/oversized/credential load repair for the banner, a persisted-
messagecredential-scrub test with a clean-message negative control, the REST + MCP cap and pass-through,
channel refusal, persistence round-trip, the
meta.nudgecontract that a bannered row's metastays byte-identical to a plain row with the banner text absent from meta, and the goal-command
producer), plus banner-restore coverage in
test/test_slot_close_nudge_race.pyand harnessupdates in the guardrail/wiring tests. All banner tests pass locally.