-
Notifications
You must be signed in to change notification settings - Fork 83
fix(transform): make the mid-turn release valve actually detect injections #273
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -139,18 +139,40 @@ function hasNewerRealUserMessage( | |
| const row = db | ||
| .prepare( | ||
| `SELECT 1 as one | ||
| FROM message | ||
| WHERE session_id = ? | ||
| AND time_created > ? | ||
| AND json_extract(data, '$.role') = 'user' | ||
| AND COALESCE(json_extract(data, '$.synthetic'), 0) NOT IN (1, 'true') | ||
| FROM message m | ||
| WHERE m.session_id = ? | ||
| AND m.time_created > ? | ||
| AND json_extract(m.data, '$.role') = 'user' | ||
| AND NOT ( | ||
| EXISTS (SELECT 1 FROM part p WHERE p.message_id = m.id) | ||
| AND NOT EXISTS ( | ||
| SELECT 1 FROM part p | ||
| WHERE p.message_id = m.id | ||
| AND COALESCE(json_extract(p.data, '$.synthetic'), 0) NOT IN (1, 'true') | ||
| AND json_extract(p.data, '$.metadata.marker.kind') IS NULL | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The whole fix now depends on two external-schema assumptions: (1) that Prompt for AI agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The self-confirming-fixture criticism is correct on method, and I've addressed it by sourcing the fixtures from real data. Both assumptions verified against a live Assumption 1 — Assumption 2 — {"marker":{"kind":"inbox","from":"…","sessionId":"…"}}
{"marker":{"kind":"interrupt","intent":"abort","origin":"parent"}}
{"marker":{"kind":"message","peer":"subagent","expectReply":false}}The fixtures now use these shapes (session id and peer name replaced with placeholders) instead of the invented On the inverse-bug risk specifically: I'd rather not rest on fixtures alone, so it's worth noting the ALL-parts semantics gives a second line of defense. For a marker path mismatch to release the lock, the message would need every part to be undetectable — and I checked what happens under ANY-semantics instead: 534 messages would flip, 525 of them genuine human prompts containing Worth flagging one thing I could not verify: |
||
| AND COALESCE(json_extract(p.data, '$.ignored'), 0) NOT IN (1, 'true') | ||
| ) | ||
| ) | ||
| LIMIT 1`, | ||
| ) | ||
| .get(sessionId, latestAssistantTimeCreated) as ExistenceRow | null; | ||
| // OpenCode persists promptAsync/channel-2 synthetic prompts as | ||
| // message.info.synthetic, which is the top-level $.synthetic field in the | ||
| // message table's data JSON. Those agent-directed nudges should not end a | ||
| // still-accumulating tool-use turn, but a later real user message does. | ||
| // OpenCode persists synthetic as an annotation on the PART row's data, never | ||
| // on the message row. So separating injected from real user messages requires | ||
| // a part join. A user message is injected iff it HAS at least one part AND | ||
| // EVERY part is machine-generated — where a part is machine-generated if it | ||
| // carries either synthetic=true, a marker part (metadata.marker.kind), or | ||
| // an ignored flag. Marker parts are deliberately NON-synthetic so the TUI | ||
| // renders them as visible system-event lines; they are identified | ||
| // structurally. Ignored parts are dropped by opencode's own model-facing | ||
| // serializer (message-v2.ts:206): an ignored text part is never pushed into | ||
| // the model-facing message, so a message whose parts are all ignored cannot | ||
| // constitute a real user turn. ALL-parts semantics is load-bearing: a real | ||
| // operator prompt may include a synthetic `agent` part from an @mention — | ||
| // classifying that as injected would release the mid-turn lock on genuine | ||
| // human input (the inverse bug, and worse). The EXISTS guard on part rows is | ||
| // the vacuous-ALL fence: a partless message satisfies "every part is | ||
| // machine-generated" trivially, so it must count as real to avoid incorrectly | ||
| // suppressing a lock release. | ||
| return row?.one === 1; | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.