mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 20:21:39 +03:00
fix(core): report a refused Console auto-watch (#63217)
This commit is contained in:
@@ -206,6 +206,19 @@ export const make = Effect.fnUntraced(function* (ctx: Context, http: HttpClient.
|
||||
// Off the tool call's path: the result never waits on Console, and a failure only skips the watch.
|
||||
return create(member, event.sessionID, { target }).pipe(
|
||||
Effect.scoped,
|
||||
// A refusal, such as a GitHub sign-in the member hasn't made, is told once per reason: the ID repeats with it.
|
||||
Effect.catchIf(
|
||||
(cause): cause is ToolFailure => cause instanceof ToolFailure && cause.metadata?.status === 403,
|
||||
(refusal) =>
|
||||
ctx.session.synthetic({
|
||||
sessionID: event.sessionID,
|
||||
id: SessionMessage.ID.make(
|
||||
`msg_watch_${Hash.sha256(`${event.sessionID}\n${refusal.message}`).slice(0, 26)}`,
|
||||
),
|
||||
text: `OpenCode Console didn't watch a pull request or push from this session, so its updates won't wake the session: ${refusal.message}`,
|
||||
metadata: { source: "console.watch" },
|
||||
}),
|
||||
),
|
||||
Effect.catch((cause) => Effect.logDebug("skipped watching a shell command's work", { cause })),
|
||||
Effect.forkIn(lease, { startImmediately: true }),
|
||||
Effect.asVoid,
|
||||
@@ -245,7 +258,7 @@ export const make = Effect.fnUntraced(function* (ctx: Context, http: HttpClient.
|
||||
editor.add({
|
||||
name: "watch",
|
||||
description:
|
||||
"Wake this session when a Slack thread or a GitHub pull request, issue, commit or Actions run gets a reply or changes status, or one of the workspace's MCP servers sends an event `events` lists. Slack and GitHub writes through OpenCode Console, `gh pr create` and `git push` are already watched, a pull request's checks included, so don't watch what you just made. Use it for anything else, such as a deploy's Actions run, `gh workflow run` or a link the user shared. People often answer a Slack post in the channel rather than its thread, so after each top-level post the channel's new top-level messages are watched for 15 minutes too. The session wakes with the update, so don't poll.",
|
||||
"Wake this session when a Slack thread or a GitHub pull request, issue, commit or Actions run gets a reply or changes status, or one of the workspace's MCP servers sends an event `events` lists. Slack and GitHub writes through OpenCode Console, `gh pr create` and `git push` are already watched, a pull request's checks included, so don't watch what you just made (the session is told if Console refuses). Use it for anything else, such as a deploy's Actions run, `gh workflow run` or a link the user shared. People often answer a Slack post in the channel rather than its thread, so after each top-level post the channel's new top-level messages are watched for 15 minutes too. The session wakes with the update, so don't poll.",
|
||||
input: Schema.Struct({
|
||||
target: Schema.optional(
|
||||
Schema.String.annotate({
|
||||
@@ -590,7 +603,7 @@ function expectOk(response: HttpClientResponse.HttpClientResponse) {
|
||||
: response.status === 404
|
||||
? "Event watches are off for this OpenCode Console workspace."
|
||||
: (body?.message ?? `OpenCode Console answered ${response.status}`)
|
||||
return Effect.fail(new ToolFailure({ message }))
|
||||
return Effect.fail(new ToolFailure({ message, metadata: { status: response.status } }))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -78,6 +78,8 @@ function fakeConsole(options: { readonly upgrade?: boolean } = {}) {
|
||||
requests: new Array<{ method: string; path: string; body?: unknown }>(),
|
||||
/** What `GET /api/event-watches/types` lists. */
|
||||
types: new Array<Record<string, unknown>>(),
|
||||
/** The status and message Console answers `POST /api/event-watches` with instead of a watch, when set. */
|
||||
refuse: undefined as { status: number; message: string } | undefined,
|
||||
}
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
@@ -112,6 +114,8 @@ function fakeConsole(options: { readonly upgrade?: boolean } = {}) {
|
||||
}
|
||||
if (url.pathname === "/api/event-watches/types") return Response.json({ types: state.types })
|
||||
if (url.pathname === "/api/event-watches" && request.method === "POST") {
|
||||
if (state.refuse)
|
||||
return Response.json({ _tag: "WatchRefused", message: state.refuse.message }, { status: state.refuse.status })
|
||||
const created = body as { sessionID: string; server?: string; event?: string; arguments?: unknown }
|
||||
// Another server's event type is keyed by a hash of it, as Console keys it.
|
||||
return Response.json({
|
||||
@@ -935,6 +939,44 @@ describe("OpencodePlugin event watches", () => {
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("tells the session once why Console refused a shell command's watch, and skips other failures", () =>
|
||||
withConsole({}, (console) =>
|
||||
Effect.gen(function* () {
|
||||
const hooks = yield* PluginHooks.Service
|
||||
const { host, wakes, replays } = yield* start(console)
|
||||
const session = yield* host.session.create({})
|
||||
const created = () =>
|
||||
console.state.requests.filter((request) => request.method === "POST" && request.path === "/api/event-watches")
|
||||
const push = (refuse: { status: number; message: string }, posted: number) =>
|
||||
Effect.gen(function* () {
|
||||
console.state.refuse = refuse
|
||||
yield* hooks.trigger("tool", "execute.after", {
|
||||
tool: "shell",
|
||||
sessionID: session.id,
|
||||
agent: Agent.ID.make("build"),
|
||||
messageID: SessionMessage.ID.create(),
|
||||
id: Tool.CallID.make("call_shell"),
|
||||
input: { command: "gh pr create --fill" },
|
||||
status: "completed",
|
||||
result: { output: { exit: 0, output: "https://github.com/Acme/App/pull/42\n", truncated: false } },
|
||||
})
|
||||
yield* waitUntil(Effect.sync(() => created().length === posted))
|
||||
})
|
||||
const refused = { status: 403, message: "Sign in to GitHub at https://console.test/github first." }
|
||||
|
||||
yield* push({ status: 500, message: "Something went wrong" }, 1)
|
||||
yield* push(refused, 2)
|
||||
yield* waitUntil(Effect.sync(() => wakes.length === 1))
|
||||
yield* push(refused, 3)
|
||||
yield* waitUntil(Effect.sync(() => replays.length === 1))
|
||||
|
||||
expect(wakes).toHaveLength(1)
|
||||
expect(wakes[0]?.sessionID).toBe(session.id)
|
||||
expect(wakes[0]?.text).toContain(refused.message)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("starts and stops with the header on the Console's periodic config check", () =>
|
||||
withConsole({}, (console) =>
|
||||
Effect.gen(function* () {
|
||||
|
||||
Reference in New Issue
Block a user