From 21215210e3d62d9c2d84fcb84d4de1b340d44cd2 Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Sun, 1 Nov 2026 02:36:44 -0500 Subject: [PATCH] Contract: pass approval option warnings, omit threadId on host pushes --- docs/api.md | 7 ++++--- gateway/src/contract.ts | 7 ++++--- gateway/src/main.ts | 2 +- gateway/src/notify.ts | 2 +- gateway/src/views.ts | 2 +- gateway/test/views.test.ts | 2 +- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/api.md b/docs/api.md index 94dc095..1d4230f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -87,7 +87,8 @@ PendingRequest { } ``` -For questions (`kind: "user_input"`), `options` is empty and `questions` lists +An option may also carry `warning`, a short note about a risky choice such as "Applies to every +thread". For questions (`kind: "user_input"`), `options` is empty and `questions` lists `{ id, header, question, options: [{ label, value }], allowCustomAnswer }`. ## Thread @@ -145,8 +146,8 @@ The new thread uses the project's default model, or the host's, and the host's d ## Notifications The gateway pushes when a thread starts waiting for you, finishes, or fails. Each push has an -`aps.category` and the custom keys `hostId`, `threadId`, and, for approvals and questions, -`requestId`. +`aps.category` and the custom keys `hostId`, `threadId` (absent on `host` pushes), and, for +approvals and questions, `requestId`. | Category | When | Actions the watch registers | | --- | --- | --- | diff --git a/gateway/src/contract.ts b/gateway/src/contract.ts index b3a1c3d..7ab2a61 100644 --- a/gateway/src/contract.ts +++ b/gateway/src/contract.ts @@ -21,8 +21,8 @@ export interface PendingRequest { /** What the agent wants to do, such as the command line. Null when the server gave none. */ prompt: string | null; reason: string | null; - /** Approval choices, in the server's order. Empty for questions. */ - options: Array<{ decision: ApprovalDecision; label: string }>; + /** Approval choices, in the server's order. Empty for questions. `warning` flags a risky choice. */ + options: Array<{ decision: ApprovalDecision; label: string; warning?: string }>; /** Questions for the user. Empty for approvals. */ questions: Array<{ id: string; @@ -126,7 +126,8 @@ export interface ErrorResponse { /** Custom keys on every push notification, beside `aps`. */ export interface PushPayload { hostId: string; - threadId: string; + /** Absent on `host` pushes, which are about a host rather than a thread. */ + threadId?: string; requestId?: string; } diff --git a/gateway/src/main.ts b/gateway/src/main.ts index 76000b5..8f80dc0 100644 --- a/gateway/src/main.ts +++ b/gateway/src/main.ts @@ -35,7 +35,7 @@ const sendPush: SendPush = async (push) => { alert: { title: push.title, ...(push.subtitle ? { subtitle: push.subtitle } : {}), body: push.body }, sound: "default", category: push.category, - "thread-id": push.data.threadId || push.data.hostId, + "thread-id": push.data.threadId ?? push.data.hostId, }, ...push.data, }; diff --git a/gateway/src/notify.ts b/gateway/src/notify.ts index e6060c1..ca9304f 100644 --- a/gateway/src/notify.ts +++ b/gateway/src/notify.ts @@ -79,7 +79,7 @@ export class Notifier { title: label, body, category: "host", - data: { hostId, threadId: "" }, + data: { hostId }, collapseId: `host:${hostId}`, }).catch(() => {}); } diff --git a/gateway/src/views.ts b/gateway/src/views.ts index 37887c1..e2097a5 100644 --- a/gateway/src/views.ts +++ b/gateway/src/views.ts @@ -64,7 +64,7 @@ function pendingRequest(thread: ThreadShell, turnItems: BoundedThread["projectio ); if (item && isApprovalRequest(item)) { const options = item.options?.length - ? item.options.map(({ decision, label }) => ({ decision, label })) + ? item.options.map(({ decision, label, warning }) => ({ decision, label, ...(warning ? { warning } : {}) })) : [ { decision: "accept" as const, label: "Allow" }, { decision: "decline" as const, label: "Deny" }, diff --git a/gateway/test/views.test.ts b/gateway/test/views.test.ts index b19f84d..3effe33 100644 --- a/gateway/test/views.test.ts +++ b/gateway/test/views.test.ts @@ -69,7 +69,7 @@ describe("summarize", () => { reason: null, options: [ { decision: "accept", label: "Allow once" }, - { decision: "acceptAlways", label: "Always allow" }, + { decision: "acceptAlways", label: "Always allow", warning: "Applies to every thread" }, { decision: "decline", label: "Deny" }, ], questions: [],