Skip to content

fix(approvals): harden question responses and payloads - #454

Open
dimakis wants to merge 6 commits into
mainfrom
fix/structured-questions-approvals
Open

dimakis wants to merge 6 commits into
mainfrom
fix/structured-questions-approvals

Conversation

@dimakis

@dimakis dimakis commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Summary

Structured questions and approval-card support reached main through later protocol and permission work while this PR was open. This branch is rebuilt on current main and now contains only the remaining hardening work from the original review:

  • cap approval-card tool input at 10,000 characters while preserving the complete input passed back to the SDK
  • reject whitespace-only answers consistently in both WebSocket protocol versions
  • keep rejected/expired responses retryable and return visible feedback to the submitting client
  • restore elevated and unknown risk-tier border cues on approval cards

Safety

  • large Write/Bash payloads no longer produce unbounded WebSocket messages or <pre> content
  • validation failure does not dismiss the pending card; the client receives a generic error without exposing server state
  • successful SDK resolution still receives the original, untruncated tool input

Validation

  • npm test -- --maxWorkers=4 (304 files; 4,361 passed, 10 skipped)
  • focused protocol, harness, client, UI, and WebSocket tests (314 passed)
  • npm run build:all
  • npm run lint (0 errors; 4 existing warnings)
  • npm run format:check

No production deployment or model call was made.

@dimakis dimakis left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Centaur Review

Found 3 issue(s) (2 warning).

packages/harness/src/permissions.ts

Solid redesign of approvals and questions with good test coverage; the main issue is an off-by-one between the frontend answer construction (up to 9 for multi-select + free text) and the backend validation (max 8), which silently drops the user's answer.

  • 🟡 bugs: Off-by-one in multi-select answer validation. resolvePending rejects answer.length > 8 (max 8 per question), but the frontend's PermissionBanner constructs answers as [...selections, ...writtenText] — if a multi-select question has 8 options (the Zod schema max) and the user selects all 8 plus writes a free-text answer, the array has 9 entries and is silently rejected. The client has already cleared the banner via respondToPermission, so the user cannot retry; the request times out. Fix: change the limit to answer.length > 9 or cap the combined frontend array at 8. [fixable]

packages/harness/src/permission-handler.ts

Solid redesign of approvals and questions with good test coverage; the main issue is an off-by-one between the frontend answer construction (up to 9 for multi-select + free text) and the backend validation (max 8), which silently drops the user's answer.

  • 🟡 bugs: Abort and timeout handlers send duplicate events. wrappedResolve (called via resolvePending) sends permission_resolved to the transport, but then the calling code in both the onAbort handler and the setTimeout callback also sends permission_timeout for the same permId. The client is idempotent (both map to PERMISSION_TIMEOUT), so no user-visible breakage, but the server sends two transport messages per resolution in these paths instead of one. Remove the transportSend(…permission_timeout…) lines from the abort and timeout handlers since wrappedResolve already broadcasts permission_resolved. [fixable]

frontend/src/components/__tests__/PermissionBanner.test.tsx

Solid redesign of approvals and questions with good test coverage; the main issue is an off-by-one between the frontend answer construction (up to 9 for multi-select + free text) and the backend validation (max 8), which silently drops the user's answer.

  • 🔵 style (L90): The four new test cases (renders questions as choices…, accepts a free-text answer…, uses the server deadline…, shows full approval input…) are top-level it() calls outside the existing describe('PermissionBanner', …) block which closes at line 89. They will run but won't appear under the 'PermissionBanner' group in test output. Move them inside the describe block. [fixable]

}
});
});

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🔵 style: The four new test cases (renders questions as choices…, accepts a free-text answer…, uses the server deadline…, shows full approval input…) are top-level it() calls outside the existing describe('PermissionBanner', …) block which closes at line 89. They will run but won't appear under the 'PermissionBanner' group in test output. Move them inside the describe block. [fixable]

@dimakis

dimakis commented Sep 7, 2026

Copy link
Copy Markdown
Owner Author

Centaur Review

Found 4 issue(s) (1 warning).

server/ws-handler-v2.ts

Well-structured PR with good test coverage. One moderate concern: the sendBootContext permission replay lacks the same error protection that permission-handler.ts deliberately applies, risking boot context loss on transport failure during reconnect. The remaining findings are type completeness gaps.

  • 🟡 bugs (L178): sendBootContext replays pending permission requests via bare conn.transport.send() without try/catch. If any request's send throws (e.g. transport closes mid-replay), the loop aborts and the subsequent boot context send is also skipped. The permission-handler.ts deliberately wraps sends in transportSend() with try/catch for exactly this scenario — the replay path should use the same protection or its own try/catch around each iteration. [fixable]

packages/client/__tests__/protocol-parser.test.ts

Well-structured PR with good test coverage. One moderate concern: the sendBootContext permission replay lacks the same error protection that permission-handler.ts deliberately applies, risking boot context loss on transport failure during reconnect. The remaining findings are type completeness gaps.

  • 🔵 missing_tests: The parser now handles a new permission_resolved server event (falls through to the permission_timeout case). There is no test that a raw { type: 'permission_resolved', permId: '...' } message correctly produces a PERMISSION_TIMEOUT action. The backend tests cover emitting this event, but the client parser's new code path has no direct coverage. [fixable]

frontend/src/types/ws-messages.ts

Well-structured PR with good test coverage. One moderate concern: the sendBootContext permission replay lacks the same error protection that permission-handler.ts deliberately applies, risking boot context loss on transport failure during reconnect. The remaining findings are type completeness gaps.

  • 🔵 regressions (L112): PermissionRequestMsg adds questions and expiresAt but is still missing sessionId, which the server now includes in every permission request (see PermissionRequest in @mitzo/protocol) and which the protocol parser extracts at protocol-parser.ts:387. Not a runtime bug (parser casts from raw JSON), but the type contract is incomplete and will mislead anyone reading the interface. [fixable]
  • 🔵 style (L125): The ServerMessage union includes PermissionTimeoutMsg but no type for the new permission_resolved event. The parser handles both, but adding an explicit type (even if it's the same shape) would document the new protocol event at the type level. [fixable]

@dimakis dimakis left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Centaur Review

Found 6 issue(s) (3 warning).

packages/harness/src/permission-handler.ts

Solid feature addition with good test coverage for the happy paths. Main concerns: unbounded toolInput sent over WebSocket (removed truncation without a replacement cap), and silent failures when server-side answer validation rejects a client submission — the client clears the banner but the server never resolves the permission.

  • 🟡 bugs (L168): toolInput is now sent unbounded to the client. Previously summarizeToolInput truncated to ~200 chars. A Write tool call with a large file body or a Bash heredoc could produce a multi-MB toolInput string sent over the WebSocket and rendered in a <pre> tag. Consider adding a size cap (e.g., truncate after 10KB) to the string sent in the permission request, while keeping the full _toolInput object for the SDK resolve path. [fixable]

packages/harness/src/permissions.ts

Solid feature addition with good test coverage for the happy paths. Main concerns: unbounded toolInput sent over WebSocket (removed truncation without a replacement cap), and silent failures when server-side answer validation rejects a client submission — the client clears the banner but the server never resolves the permission.

  • 🟡 bugs (L53): For single-select questions (multiSelect: false), answer.length !== 1 rejects answers. But the frontend builds answers by concatenating selections + written text. If a user selects a radio option and then types free text without the clearing handler firing (e.g., programmatic state, tests, or a race with React batching), both would be present and the server would silently reject with return false. The v1 WS handler at server/index.ts:893 also doesn't pass sessionId, so resolvePending skips session validation for v1 clients — a silent rejection leaves the permission pending until timeout with no feedback to the user. [fixable]
  • 🔵 unsafe_assumptions (L54): Whitespace-only answer validation mismatch: resolvePending rejects whitespace-only strings via !value.trim(), but the Zod schemas in ws-schemas-v2.ts and ws-schemas.ts use z.string().min(1) which accepts whitespace-only strings like " ". A client-submitted whitespace-only answer passes schema validation but silently fails in resolvePending. Aligning these (e.g., adding .trim().min(1) to the Zod schema or .transform(s => s.trim())) would surface the error earlier. [fixable]

server/ws-handler-v2.ts

Solid feature addition with good test coverage for the happy paths. Main concerns: unbounded toolInput sent over WebSocket (removed truncation without a replacement cap), and silent failures when server-side answer validation rejects a client submission — the client clears the banner but the server never resolves the permission.

  • 🟡 bugs (L811): resolvePending returns false when validation fails (wrong answer count, whitespace-only values, always decision on a question, missing session match). The return value is silently ignored here and at server/index.ts:893. When validation fails, the permission stays deleted (it's not — pending.delete only runs after validation passes, which is correct), but the user sees the card disappear (the client dispatches PERMISSION_TIMEOUT locally in respondToPermission) while the server-side promise never resolves. The permission hangs until the 120s timeout fires server-side, while the client has already cleared the banner. The user has no way to retry. [fixable]

frontend/src/styles/global.css

Solid feature addition with good test coverage for the happy paths. Main concerns: unbounded toolInput sent over WebSocket (removed truncation without a replacement cap), and silent failures when server-side answer validation rejects a client submission — the client clears the banner but the server never resolves the permission.

  • 🔵 regressions: The .perm-banner--elevated and .perm-banner--unknown classes previously set border-top-color to visually distinguish elevated/unknown tier banners. The new CSS removes both rules without a replacement visual indicator. Elevated and unknown tier banners now look identical to standard ones aside from the tier badge text. The old desktop-specific .perm-banner override (centered, max-width 480px) is also removed — the new base styles handle this, but the max-width changed from 480px to 600px. [fixable]

packages/harness/__tests__/user-questions.test.ts

Solid feature addition with good test coverage for the happy paths. Main concerns: unbounded toolInput sent over WebSocket (removed truncation without a replacement cap), and silent failures when server-side answer validation rejects a client submission — the client clears the banner but the server never resolves the permission.

  • 🔵 missing_tests: No test covers the case where resolvePending returns false at the WS handler layer and the user gets no feedback. Also missing: a test for single-select questions where the user provides only a free-text answer (no option selected), which is a valid UX path in the PermissionBanner component. [fixable]

permId,
toolName,
toolInput: inputSummary,
toolInput: questions

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 bugs: toolInput is now sent unbounded to the client. Previously summarizeToolInput truncated to ~200 chars. A Write tool call with a large file body or a Bash heredoc could produce a multi-MB toolInput string sent over the WebSocket and rendered in a <pre> tag. Consider adding a size cap (e.g., truncate after 10KB) to the string sent in the permission request, while keeping the full _toolInput object for the SDK resolve path. [fixable]

!Array.isArray(answer) ||
!answer.length ||
answer.length > 9 ||
(!question.multiSelect && answer.length !== 1) ||

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 bugs: For single-select questions (multiSelect: false), answer.length !== 1 rejects answers. But the frontend builds answers by concatenating selections + written text. If a user selects a radio option and then types free text without the clearing handler firing (e.g., programmatic state, tests, or a race with React batching), both would be present and the server would silently reject with return false. The v1 WS handler at server/index.ts:893 also doesn't pass sessionId, so resolvePending skips session validation for v1 clients — a silent rejection leaves the permission pending until timeout with no feedback to the user. [fixable]

Comment thread packages/harness/src/permissions.ts Outdated
!answer.length ||
answer.length > 9 ||
(!question.multiSelect && answer.length !== 1) ||
answer.some((value) => typeof value !== 'string' || !value.trim() || value.length > 4000)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🔵 unsafe_assumptions: Whitespace-only answer validation mismatch: resolvePending rejects whitespace-only strings via !value.trim(), but the Zod schemas in ws-schemas-v2.ts and ws-schemas.ts use z.string().min(1) which accepts whitespace-only strings like " ". A client-submitted whitespace-only answer passes schema validation but silently fails in resolvePending. Aligning these (e.g., adding .trim().min(1) to the Zod schema or .transform(s => s.trim())) would surface the error earlier. [fixable]

Comment thread server/ws-handler-v2.ts Outdated
},
() => {
resolvePending(msg.permId, msg.decision ?? 'deny');
resolvePending(msg.permId, msg.decision ?? 'deny', msg.answers, msg.sessionId);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 bugs: resolvePending returns false when validation fails (wrong answer count, whitespace-only values, always decision on a question, missing session match). The return value is silently ignored here and at server/index.ts:893. When validation fails, the permission stays deleted (it's not — pending.delete only runs after validation passes, which is correct), but the user sees the card disappear (the client dispatches PERMISSION_TIMEOUT locally in respondToPermission) while the server-side promise never resolves. The permission hangs until the 120s timeout fires server-side, while the client has already cleared the banner. The user has no way to retry. [fixable]

@dimakis
dimakis force-pushed the fix/structured-questions-approvals branch from 55f3962 to c962739 Compare September 15, 2026 07:54
@dimakis dimakis changed the title fix: restore structured questions and redesign approval cards fix(approvals): harden question responses and payloads Sep 15, 2026

@dimakis dimakis left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Centaur Review

Found 3 issue(s) (3 warning).

packages/harness/src/permission-handler.ts

The PR improves bounds checking but introduces an unsafe truncated-approval view and two response-path regressions involving terminal error handling and destructive whitespace normalization.

  • 🟡 unsafe_assumptions (L47): The approval payload now shows only the first 10,000 characters while resolvePending still executes the complete input. A long Bash command or external-tool payload can therefore hide consequential operations after the truncation marker and receive approval without the user seeing them. The approval boundary should expose the full input (for example via an expandable/detail channel) or reject inputs that cannot be reviewed safely. [fixable]

server/ws-handler-v2.ts

The PR improves bounds checking but introduces an unsafe truncated-approval view and two response-path regressions involving terminal error handling and destructive whitespace normalization.

  • 🟡 regressions (L935): A failed/stale/duplicate permission response is emitted as the generic error event. The client handles every such event as terminal: its ERROR reducer sets running=false and clears the current streaming message. A timeout race or double-click can therefore discard the in-progress assistant/tool message even though the permission remains pending or was already resolved. Use a permission-specific nonterminal rejection event (and make the same change in the legacy handler in server/index.ts) instead of error. [fixable]

packages/protocol/src/ws-schemas-v2.ts

The PR improves bounds checking but introduces an unsafe truncated-approval view and two response-path regressions involving terminal error handling and destructive whitespace normalization.

  • 🟡 regressions (L123): Using .trim() in the schema transforms valid answer keys and values rather than merely validating them. Question IDs and option labels are not trimmed when requests are registered, so a provider question whose ID or allowed option has leading/trailing whitespace becomes impossible to resolve; free-form answers are also silently changed before reaching the SDK. Validate value.trim().length > 0 with a refinement while preserving the original strings, and mirror the fix in ws-schemas.ts. [fixable]

toolName === 'Bash' && typeof input.command === 'string'
? input.command
: JSON.stringify(input, null, 2);
return full.length > PERMISSION_INPUT_MAX_CHARS

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 unsafe_assumptions: The approval payload now shows only the first 10,000 characters while resolvePending still executes the complete input. A long Bash command or external-tool payload can therefore hide consequential operations after the truncation marker and receive approval without the user seeing them. The approval boundary should expose the full input (for example via an expandable/detail channel) or reject inputs that cannot be reviewed safely. [fixable]

Comment thread server/ws-handler-v2.ts Outdated
);
if (!resolved) {
try {
ctx.connRegistry.get(connectionId)?.transport.send({

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 regressions: A failed/stale/duplicate permission response is emitted as the generic error event. The client handles every such event as terminal: its ERROR reducer sets running=false and clears the current streaming message. A timeout race or double-click can therefore discard the in-progress assistant/tool message even though the permission remains pending or was already resolved. Use a permission-specific nonterminal rejection event (and make the same change in the legacy handler in server/index.ts) instead of error. [fixable]

Comment thread packages/protocol/src/ws-schemas-v2.ts Outdated
answers: z
.record(z.string().min(1).max(4000), z.array(z.string().min(1).max(4000)).min(1).max(9))
.record(
z.string().trim().min(1).max(4000),

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 regressions: Using .trim() in the schema transforms valid answer keys and values rather than merely validating them. Question IDs and option labels are not trimmed when requests are registered, so a provider question whose ID or allowed option has leading/trailing whitespace becomes impossible to resolve; free-form answers are also silently changed before reaching the SDK. Validate value.trim().length > 0 with a refinement while preserving the original strings, and mirror the fix in ws-schemas.ts. [fixable]

@dimakis

dimakis commented Sep 15, 2026

Copy link
Copy Markdown
Owner Author

Centaur Review

Found 3 issue(s) (3 warning).

packages/protocol/src/ws-schemas-v2.ts

The hardening is directionally sound, but accepted provider IDs can become unanswerable, schema-level rejections remain silent, and the retry alert layout breaks on narrow screens.

  • 🟡 regressions (L128): The new NonBlankPermissionText constraint is also applied to record keys, but provider question IDs are opaque and upstream CodexUserInput accepts whitespace-only and >4000-character IDs. Such accepted questions can now be displayed but every answer payload for them is rejected, leaving the interaction impossible to complete. Validate answer values here, or impose the same ID constraints when accepting questions (and apply the equivalent fix to the v1 schema). [fixable]

server/ws-handler-v2.ts

The hardening is directionally sound, but accepted provider IDs can become unanswerable, schema-level rejections remain silent, and the retry alert layout breaks on narrow screens.

  • 🟡 regressions (L1289): Whitespace-only answers are rejected by IncomingWsMessageV2.safeParse before handlePermissionResponseV2 runs, and this failure path silently returns. Consequently the newly added permission_response_rejected event and retry error are never sent for the exact malformed payload this PR newly rejects; the client sees an apparently inert Send action while the prompt remains pending. Detect invalid permission_response messages in this branch and return a rejection event (with equivalent handling in the legacy dispatcher). [fixable]

frontend/src/styles/global.css

The hardening is directionally sound, but accepted provider IDs can become unanswerable, schema-level rejections remain silent, and the retry alert layout breaks on narrow screens.

  • 🟡 bugs (L3382): The error paragraph uses flex-basis: 100%, but .perm-banner-actions does not enable flex-wrap. The alert therefore stays on the same flex row as the action buttons, compressing the alert and potentially overflowing the approval controls on mobile instead of occupying its own row. Add wrapping (or place the alert outside the button flex row). [fixable]

@dimakis dimakis left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Centaur Review

Found 1 issue(s) (1 warning).

packages/harness/src/permission-handler.ts

The approval hardening is coherent, but the new payload-size helper introduces a strict TypeScript build failure.

  • 🟡 bugs (L49): JSON.stringify() is typed as returning string | undefined, so full.length fails strict TypeScript compilation (full is possibly undefined). Handle an undefined serialization result before reading .length. [fixable]

toolName === 'Bash' && typeof input.command === 'string'
? input.command
: JSON.stringify(input, null, 2);
return full.length <= PERMISSION_INPUT_MAX_CHARS ? full : undefined;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 bugs: JSON.stringify() is typed as returning string | undefined, so full.length fails strict TypeScript compilation (full is possibly undefined). Handle an undefined serialization result before reading .length. [fixable]

@dimakis dimakis left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Centaur Review

Found 2 issue(s) (2 warning).

packages/protocol/src/ws-schemas-v2.ts

Two approval-hardening gaps remain: malformed SSE responses can destroy client stream state, and throwing serialization failures escape the safe-denial path.

  • 🟡 bugs (L127): Schema-invalid permission answers are not retry-safe on the default browser SSE transport. POST /api/chat/permission rejects them in validateBody before handlePermissionResponseV2 can emit permission_response_rejected; SseConnection converts that 400 into a generic ERROR action, which clears current streaming content and sets running=false while the server is still waiting on the permission. Handle permission validation failures specially in the REST route/client so they dispatch PERMISSION_REJECTED and preserve stream state. [fixable]

packages/harness/src/permission-handler.ts

Two approval-hardening gaps remain: malformed SSE responses can destroy client stream state, and throwing serialization failures escape the safe-denial path.

  • 🟡 unsafe_assumptions (L48): The new serialization guard only handles JSON.stringify returning undefined; JSON.stringify can also throw for circular objects, BigInt values, or throwing toJSON methods. Because the permission handler is async, such input rejects the handler instead of returning the intended safe denial. Catch serialization errors and return undefined, with a regression test using a throwing/circular input. [fixable]

answers: z
.record(z.string().min(1).max(4000), z.array(z.string().min(1).max(4000)).min(1).max(9))
.optional(),
answers: z.record(z.string(), z.array(NonBlankPermissionText).min(1).max(9)).optional(),

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 bugs: Schema-invalid permission answers are not retry-safe on the default browser SSE transport. POST /api/chat/permission rejects them in validateBody before handlePermissionResponseV2 can emit permission_response_rejected; SseConnection converts that 400 into a generic ERROR action, which clears current streaming content and sets running=false while the server is still waiting on the permission. Handle permission validation failures specially in the REST route/client so they dispatch PERMISSION_REJECTED and preserve stream state. [fixable]

const full =
toolName === 'Bash' && typeof input.command === 'string'
? input.command
: JSON.stringify(input, null, 2);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 unsafe_assumptions: The new serialization guard only handles JSON.stringify returning undefined; JSON.stringify can also throw for circular objects, BigInt values, or throwing toJSON methods. Because the permission handler is async, such input rejects the handler instead of returning the intended safe denial. Catch serialization errors and return undefined, with a regression test using a throwing/circular input. [fixable]

@dimakis dimakis left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Centaur Review

Found 1 issue(s) (1 warning).

server/chat-rest-handler.ts

The validation and client retry state are generally sound, but semantic permission rejections can be lost on the primary SSE transport because the REST endpoint acknowledges them as successful.

  • 🟡 bugs (L247): The REST endpoint always returns {ok:true} after a schema-valid response, even when resolvePending() rejected it. handlePermissionResponseV2() reports that failure only through a non-durable SSE event, while SseConnection.doPost() interprets only non-2xx responses as submission failures. If the SSE stream closes or its write fails during the POST, the client receives a successful HTTP response and no retry error; an expired request will not be replayed, leaving the stale prompt visible. Propagate the resolution result and return a non-2xx permission_response_rejected response when it is false. [fixable]

const msg = parsed.data;
try {
handlePermissionResponseV2(connectionId, msg, ctx);
res.json({ ok: true });

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 bugs: The REST endpoint always returns {ok:true} after a schema-valid response, even when resolvePending() rejected it. handlePermissionResponseV2() reports that failure only through a non-durable SSE event, while SseConnection.doPost() interprets only non-2xx responses as submission failures. If the SSE stream closes or its write fails during the POST, the client receives a successful HTTP response and no retry error; an expired request will not be replayed, leaving the stale prompt visible. Propagate the resolution result and return a non-2xx permission_response_rejected response when it is false. [fixable]

@dimakis

dimakis commented Sep 15, 2026

Copy link
Copy Markdown
Owner Author

Centaur Review

LGTM — no issues found.

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