Contract: pass approval option warnings, omit threadId on host pushes
Gateway / check (push) Successful in 8s
Gateway / image (push) Successful in 21s

This commit is contained in:
gib committed 2026-11-01 02:36:44 -05:00
1 parent 7ad1045be4
commit 21215210e3
6 files changed
+12 -10

No files matched your search

+4 -3
View File
@@ -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 |
| --- | --- | --- |
+4 -3
View File
@@ -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;
}
+1 -1
View File
@@ -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,
};
+1 -1
View File
@@ -79,7 +79,7 @@ export class Notifier {
title: label,
body,
category: "host",
data: { hostId, threadId: "" },
data: { hostId },
collapseId: `host:${hostId}`,
}).catch(() => {});
}
+1 -1
View File
@@ -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" },
+1 -1
View File
@@ -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: [],