mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-10 10:25:03 +00:00
Show and stop agent background work (#6220)
* feat(server): add an in-memory store for agent background work Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(claude): report background tasks as background work Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(server): track agent background work in the agent manager Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(protocol): add agent background work messages Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(server): serve and push agent background work Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(client): expose agent background work on agent handles Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(sdk): document agent background work Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(claude): cover background shells in background work end to end Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(client): resync background work subscriptions on every snapshot subscribe() now lists the agent's background work each time the event observation is established, so the listener starts with the current list and catches up after a reconnect. A list answered after a newer update is dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server): answer background work lists without loading dormant agents Background work lives only in memory, so a stored agent that is not loaded has none. The list handler now answers [] from storage instead of resuming the agent, and still rejects unknown, internal, and archived agents. A new test pins that old apps never receive agent.background_work.update. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server): read Claude background tasks defensively The user's claude binary can differ from the SDK typings, so a task's description is read as unknown and an empty or missing one becomes null. Drop the monitor kind: Claude Monitor watches report as local_bash, so kind is "shell" or "other" today. The store now copies only the wire fields of each input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(server): prove internal background work was emitted before checking globals Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(sdk): describe background work resync and feature gating Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(claude): cover background work reported by a retired query The daemon stops reading a Claude query it no longer owns, so work that a retired process still had queued cannot reach the background work list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(client): let background work subscribers handle a failed list read When the list read that follows each subscription snapshot failed, the error was only logged and the caller got neither the list nor a failure. subscribe() now takes an optional onError that receives it; without one the error is still logged. The read is not retried, since the next reconnect reads the list again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(agents): show background work as active Keep background activity visible after a foreground turn ends while preserving independent send and interrupt behavior. * fix(agents): protect background work subscriptions Keep internal work out of public updates and restore observation after a subscription failure. * feat(agents): show Codex and OpenCode background work Keep provider-owned shell activity visible after a turn ends while leaving foreground turn controls independent. * feat(agents): stop individual background work Wait for provider confirmation so stopping one background item leaves sibling work and foreground turn controls independent. * refactor(agents): name background stop parameters * test(website): isolate browser tests from live metadata * test(agents): clarify background stop journeys * fix(sdk): report background work subscription failures Report setup failures so subscribers can replace an observation that the host released. * fix(app): restore background work after reconnect Wait for the host handshake before selecting the retained client so an offline pane starts observing when the connection returns. * fix(agents): retain background shells across rewind Preserve known live shell ownership when conversation history is rewound, and revoke it when the provider runtime exits. * fix(website): launch test seeder across platforms Invoke Wrangler through Node so local website test startup does not depend on an executable npm command shim. * feat(agents): clarify background task details and tracks Keep launch descriptions and commands distinct so background work remains identifiable, and allow composer tracks to scroll when space is limited. * fix(agents): preserve active background work during archive Keep background-running children out of bulk cleanup and warn before an individual archive. Preserve first taps on composer tracks while the keyboard is open. * fix(codex): retain background terminals across rewind Keep live terminal inventories attached to their originating threads so a fork does not hide or misroute their stop actions. * fix(codex): scope background terminal identities to threads Prevent commands on separate threads with the same provider process ID from sharing a stop target. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Mohamed Boudra
parent
c8bd4717c9
commit
77798b2e46
@@ -111,6 +111,7 @@ export class LegacyWorkspaces {
|
||||
pendingPermissionCount: agent.pendingPermissions.length,
|
||||
requiresAttention: agent.requiresAttention,
|
||||
attentionReason: agent.attentionReason,
|
||||
backgroundWork: agent.backgroundWork,
|
||||
});
|
||||
const existing = workspaces.get(id);
|
||||
if (
|
||||
|
||||
@@ -1049,6 +1049,9 @@ type CorrelatedResponsePayloads = {
|
||||
};
|
||||
type CorrelatedResponsePayload<TType extends CorrelatedResponseType> =
|
||||
CorrelatedResponsePayloads[TType];
|
||||
export type AgentBackgroundWorkListPayload =
|
||||
CorrelatedResponsePayload<"agent.background_work.list.response">;
|
||||
export type AgentBackgroundWorkItem = AgentBackgroundWorkListPayload["items"][number];
|
||||
|
||||
type PluginCatalogResponseEntry =
|
||||
CorrelatedResponsePayload<"plugin.catalog.get.response">["plugins"][number];
|
||||
@@ -3869,6 +3872,58 @@ export class DaemonClient {
|
||||
return payload;
|
||||
}
|
||||
|
||||
async listAgentBackgroundWork(
|
||||
agentId: string,
|
||||
options: { requestId?: string; timeout?: number } = {},
|
||||
): Promise<AgentBackgroundWorkItem[]> {
|
||||
this.assertAgentBackgroundWorkSupported();
|
||||
const payload =
|
||||
await this.sendNamespacedCorrelatedSessionRequest<"agent.background_work.list.response">({
|
||||
requestId: options.requestId,
|
||||
timeout: options.timeout,
|
||||
message: { type: "agent.background_work.list.request", agentId },
|
||||
});
|
||||
if (payload.error) throw new Error(payload.error);
|
||||
return payload.items;
|
||||
}
|
||||
|
||||
async stopAgentBackgroundWork(
|
||||
agentId: string,
|
||||
item: AgentBackgroundWorkItem,
|
||||
options: { requestId?: string; timeout?: number } = {},
|
||||
): Promise<void> {
|
||||
// COMPAT(agentBackgroundWorkStop): added after v0.15.0, remove after 2027-05-03.
|
||||
if (this.lastServerInfoMessage?.features?.agentBackgroundWorkStop !== true)
|
||||
throw new Error("Update the host to stop background work.");
|
||||
if (!item.canStop || !item.stopToken) throw new Error("Background work cannot be stopped.");
|
||||
const payload =
|
||||
await this.sendNamespacedCorrelatedSessionRequest<"agent.background_work.stop.response">({
|
||||
requestId: options.requestId,
|
||||
timeout: options.timeout ?? 30000,
|
||||
message: {
|
||||
type: "agent.background_work.stop.request",
|
||||
agentId,
|
||||
itemId: item.id,
|
||||
stopToken: item.stopToken,
|
||||
},
|
||||
});
|
||||
if (payload.error) throw new Error(payload.error);
|
||||
}
|
||||
|
||||
observeAgentBackgroundWork(
|
||||
options: { signal?: AbortSignal } = {},
|
||||
): OwnedSubscription<CorrelatedResponsePayload<"session.events.set_subscription.response">> {
|
||||
this.assertAgentBackgroundWorkSupported();
|
||||
return this.observeEvents(["agent.background_work.update"], options);
|
||||
}
|
||||
|
||||
private assertAgentBackgroundWorkSupported(): void {
|
||||
// COMPAT(agentBackgroundWork): added after v0.11.0-beta.5, remove gate after 2027-04-06.
|
||||
if (this.lastServerInfoMessage?.features?.agentBackgroundWork !== true) {
|
||||
throw new Error("Update the host to see an agent's background work.");
|
||||
}
|
||||
}
|
||||
|
||||
async fetchProviderSubagentTimeline(
|
||||
parentAgentId: string,
|
||||
subagentId: string,
|
||||
|
||||
@@ -1938,3 +1938,256 @@ test("agent reload reopens through the daemon RPC then returns the observed snap
|
||||
await client.close();
|
||||
}
|
||||
});
|
||||
|
||||
const BACKGROUND_WORK_FEATURES = { ownedSubscriptions: true, agentBackgroundWork: true };
|
||||
const SHELL_ITEM = {
|
||||
id: "bash-1",
|
||||
kind: "shell",
|
||||
description: "sleep 20",
|
||||
startedAt: "2026-10-06T10:00:00.000Z",
|
||||
};
|
||||
|
||||
function answerBackgroundWorkList(ws: FakeWebSocket, items: unknown[]): void {
|
||||
const request = parseSentSessionMessage(ws.sent.at(-1));
|
||||
expect(request).toMatchObject({ type: "agent.background_work.list.request", agentId: "agent-a" });
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "agent.background_work.list.response",
|
||||
payload: { requestId: request.requestId, agentId: "agent-a", items, error: null },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
test("agent handles list background work", async () => {
|
||||
const { client, ws } = await connectClient(BACKGROUND_WORK_FEATURES);
|
||||
|
||||
const pending = client.agents.ref("agent-a").backgroundWork.list();
|
||||
const request = parseSentSessionMessage(ws.sent.at(-1));
|
||||
expect(request).toMatchObject({ type: "agent.background_work.list.request", agentId: "agent-a" });
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "agent.background_work.list.response",
|
||||
payload: {
|
||||
requestId: request.requestId,
|
||||
agentId: "agent-a",
|
||||
items: [SHELL_ITEM],
|
||||
error: null,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(pending).resolves.toEqual([SHELL_ITEM]);
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("agent handles follow only their own background work", async () => {
|
||||
const { client, ws } = await connectClient(BACKGROUND_WORK_FEATURES);
|
||||
const received: unknown[] = [];
|
||||
|
||||
const unsubscribe = client.agents
|
||||
.ref("agent-a")
|
||||
.backgroundWork.subscribe((items) => received.push(items));
|
||||
expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({
|
||||
type: "session.events.set_subscription.request",
|
||||
events: ["agent.background_work.update"],
|
||||
});
|
||||
acknowledgeObservation(ws, "background-work-sdk");
|
||||
answerBackgroundWorkList(ws, []);
|
||||
await vi.waitFor(() => expect(received).toEqual([[]]));
|
||||
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "agent.background_work.update",
|
||||
payload: { subscriptionId: "background-work-sdk", agentId: "agent-b", items: [SHELL_ITEM] },
|
||||
}),
|
||||
);
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "agent.background_work.update",
|
||||
payload: { subscriptionId: "background-work-sdk", agentId: "agent-a", items: [SHELL_ITEM] },
|
||||
}),
|
||||
);
|
||||
await vi.waitFor(() => expect(received).toEqual([[], [SHELL_ITEM]]));
|
||||
|
||||
unsubscribe();
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "agent.background_work.update",
|
||||
payload: { subscriptionId: "background-work-sdk", agentId: "agent-a", items: [] },
|
||||
}),
|
||||
);
|
||||
expect(received).toEqual([[], [SHELL_ITEM]]);
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("background work subscriptions list the current work again after a reconnect", async () => {
|
||||
const { client, ws } = await connectClient(BACKGROUND_WORK_FEATURES);
|
||||
const received: unknown[] = [];
|
||||
|
||||
client.agents.ref("agent-a").backgroundWork.subscribe((items) => received.push(items));
|
||||
acknowledgeObservation(ws, "background-work-first");
|
||||
answerBackgroundWorkList(ws, [SHELL_ITEM]);
|
||||
await vi.waitFor(() => expect(received).toEqual([[SHELL_ITEM]]));
|
||||
|
||||
ws.readyState = 3;
|
||||
ws.onclose?.({ code: 1006, reason: "Connection lost" });
|
||||
const reconnecting = client.connect();
|
||||
const next = FakeWebSocket.instances[1];
|
||||
next.open();
|
||||
next.message(
|
||||
sessionMessage({
|
||||
type: "status",
|
||||
payload: {
|
||||
status: "server_info",
|
||||
serverId: "srv_sdk_test",
|
||||
hostname: null,
|
||||
version: null,
|
||||
features: BACKGROUND_WORK_FEATURES,
|
||||
},
|
||||
}),
|
||||
);
|
||||
await reconnecting;
|
||||
await vi.waitFor(() =>
|
||||
expect(parseSentSessionMessage(next.sent.at(-1))).toMatchObject({
|
||||
type: "session.events.set_subscription.request",
|
||||
}),
|
||||
);
|
||||
acknowledgeObservation(next, "background-work-second");
|
||||
answerBackgroundWorkList(next, []);
|
||||
|
||||
await vi.waitFor(() => expect(received).toEqual([[SHELL_ITEM], []]));
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("background work subscriptions keep an update that overtakes their list", async () => {
|
||||
const { client, ws } = await connectClient(BACKGROUND_WORK_FEATURES);
|
||||
const received: unknown[] = [];
|
||||
|
||||
client.agents.ref("agent-a").backgroundWork.subscribe((items) => received.push(items));
|
||||
acknowledgeObservation(ws, "background-work-sdk");
|
||||
await vi.waitFor(() =>
|
||||
expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({
|
||||
type: "agent.background_work.list.request",
|
||||
}),
|
||||
);
|
||||
const listRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "agent.background_work.update",
|
||||
payload: { subscriptionId: "background-work-sdk", agentId: "agent-a", items: [] },
|
||||
}),
|
||||
);
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "agent.background_work.list.response",
|
||||
payload: {
|
||||
requestId: listRequest.requestId,
|
||||
agentId: "agent-a",
|
||||
items: [SHELL_ITEM],
|
||||
error: null,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => expect(received).toEqual([[]]));
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(received).toEqual([[]]);
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("background work subscriptions report a failed setup to onError", async () => {
|
||||
const { client, ws } = await connectClient(BACKGROUND_WORK_FEATURES);
|
||||
const received: unknown[] = [];
|
||||
const errors: unknown[] = [];
|
||||
client.agents.ref("agent-a").backgroundWork.subscribe((items) => received.push(items), {
|
||||
onError: (error) => errors.push(error),
|
||||
});
|
||||
const request = parseSentSessionMessage(ws.sent.at(-1));
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "session.events.set_subscription.response",
|
||||
payload: { requestId: request.requestId },
|
||||
}),
|
||||
);
|
||||
await vi.waitFor(() => expect(errors).toHaveLength(1));
|
||||
expect((errors[0] as Error).message).toContain("subscription ID");
|
||||
expect(received).toEqual([]);
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("background work subscriptions report a failed list to onError", async () => {
|
||||
const { client, ws } = await connectClient(BACKGROUND_WORK_FEATURES);
|
||||
const received: unknown[] = [];
|
||||
const errors: unknown[] = [];
|
||||
|
||||
client.agents.ref("agent-a").backgroundWork.subscribe((items) => received.push(items), {
|
||||
onError: (error) => errors.push(error),
|
||||
});
|
||||
acknowledgeObservation(ws, "background-work-sdk");
|
||||
await vi.waitFor(() =>
|
||||
expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({
|
||||
type: "agent.background_work.list.request",
|
||||
}),
|
||||
);
|
||||
const listRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "agent.background_work.list.response",
|
||||
payload: { requestId: listRequest.requestId, agentId: "agent-a", items: [], error: "boom" },
|
||||
}),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => expect(errors).toHaveLength(1));
|
||||
expect(errors[0]).toBeInstanceOf(Error);
|
||||
expect((errors[0] as Error).message).toContain("boom");
|
||||
expect(received).toEqual([]);
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("background work asks for a host update when the daemon lacks it", async () => {
|
||||
const { client } = await connectClient({ ownedSubscriptions: true });
|
||||
const handle = client.agents.ref("agent-a");
|
||||
|
||||
await expect(handle.backgroundWork.list()).rejects.toThrow("Update the host");
|
||||
expect(() => handle.backgroundWork.subscribe(() => {})).toThrow("Update the host");
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("background stop sends the live item token and waits for the response", async () => {
|
||||
const { client, ws } = await connectClient({
|
||||
...BACKGROUND_WORK_FEATURES,
|
||||
agentBackgroundWorkStop: true,
|
||||
});
|
||||
const item = { ...SHELL_ITEM, canStop: true, stopToken: "generation-1" };
|
||||
const pending = client.agents.ref("agent-a").backgroundWork.stop(item);
|
||||
const request = parseSentSessionMessage(ws.sent.at(-1));
|
||||
expect(request).toMatchObject({
|
||||
type: "agent.background_work.stop.request",
|
||||
agentId: "agent-a",
|
||||
itemId: item.id,
|
||||
stopToken: item.stopToken,
|
||||
});
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "agent.background_work.stop.response",
|
||||
payload: {
|
||||
requestId: request.requestId,
|
||||
agentId: "agent-a",
|
||||
itemId: item.id,
|
||||
error: "rejected",
|
||||
},
|
||||
}),
|
||||
);
|
||||
await expect(pending).rejects.toThrow("rejected");
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("background stop rejects when the host does not advertise the operation", async () => {
|
||||
const { client } = await connectClient(BACKGROUND_WORK_FEATURES);
|
||||
await expect(
|
||||
client.agents
|
||||
.ref("agent-a")
|
||||
.backgroundWork.stop({ ...SHELL_ITEM, canStop: true, stopToken: "generation" }),
|
||||
).rejects.toThrow("Update the host");
|
||||
await client.close();
|
||||
});
|
||||
|
||||
@@ -54,6 +54,7 @@ export type {
|
||||
} from "./terminals/index.js";
|
||||
import type { PluginTimelineItem } from "@getpaseo/protocol/agent-types";
|
||||
import type {
|
||||
AgentBackgroundWorkItem,
|
||||
FetchAgentsEntry,
|
||||
FetchAgentsOptions,
|
||||
FetchAgentsPageInfo,
|
||||
@@ -353,6 +354,36 @@ export interface PaseoAgentTimelineHandle {
|
||||
subscribe(handler: (event: PaseoAgentTimelineEvent) => void): PaseoAgentTimelineSubscription;
|
||||
}
|
||||
|
||||
export type PaseoAgentBackgroundWorkItem = AgentBackgroundWorkItem;
|
||||
|
||||
export interface PaseoAgentBackgroundWorkHandle {
|
||||
/** Stop one item and wait for provider confirmation. */
|
||||
stop(
|
||||
item: PaseoAgentBackgroundWorkItem,
|
||||
options?: { requestId?: string; timeout?: number },
|
||||
): Promise<void>;
|
||||
/** Reads the agent's current background work once. */
|
||||
list(options?: { requestId?: string; timeout?: number }): Promise<PaseoAgentBackgroundWorkItem[]>;
|
||||
/**
|
||||
* Calls `listener` with the agent's current list once the subscription is established, again
|
||||
* after each reconnect, then with the full list each time it changes. Returns the unsubscribe
|
||||
* function.
|
||||
*
|
||||
* When reading the current list fails, `options.onError` receives the error and `listener` is
|
||||
* not called; without `onError` the error is logged. The read is not retried: the next
|
||||
* reconnect reads the list again.
|
||||
*/
|
||||
subscribe(
|
||||
listener: (items: PaseoAgentBackgroundWorkItem[]) => void,
|
||||
options?: PaseoAgentBackgroundWorkSubscribeOptions,
|
||||
): () => void;
|
||||
}
|
||||
|
||||
export interface PaseoAgentBackgroundWorkSubscribeOptions {
|
||||
/** Receives the error when reading the agent's current list fails. */
|
||||
onError?: (error: unknown) => void;
|
||||
}
|
||||
|
||||
export interface PaseoAgentHandle {
|
||||
readonly id: string;
|
||||
readonly address: AgentAddress;
|
||||
@@ -376,6 +407,7 @@ export interface PaseoAgentHandle {
|
||||
readonly runtimeInfo: NonNullable<PaseoAgent["runtimeInfo"]> | null;
|
||||
readonly archivedAt: NonNullable<PaseoAgent["archivedAt"]> | null;
|
||||
readonly timeline: PaseoAgentTimelineHandle;
|
||||
readonly backgroundWork: PaseoAgentBackgroundWorkHandle;
|
||||
current(): PaseoAgent | null;
|
||||
refresh(requestId?: string): Promise<PaseoAgentRefetchResult | null>;
|
||||
/**
|
||||
@@ -612,6 +644,7 @@ export function createPaseoApi(
|
||||
daemonClient,
|
||||
listenAgents,
|
||||
(agentId, handler) => own(() => daemonClient.subscribeAgentTimeline(agentId, handler)),
|
||||
() => own(() => daemonClient.observeAgentBackgroundWork()),
|
||||
);
|
||||
const createAgent = async (
|
||||
options: PaseoAgentCreateOptions,
|
||||
@@ -893,6 +926,7 @@ function createAgentHandleFactory(
|
||||
daemonClient: DaemonClient,
|
||||
listen: (handler: PaseoAgentUpdateHandler) => () => void,
|
||||
subscribeTimeline: DaemonClient["subscribeAgentTimeline"],
|
||||
observeBackgroundWork: () => ReturnType<DaemonClient["observeAgentBackgroundWork"]>,
|
||||
): AgentHandleFactory {
|
||||
return (agent) => {
|
||||
const id = typeof agent === "string" ? agent : agent.id;
|
||||
@@ -939,6 +973,48 @@ function createAgentHandleFactory(
|
||||
}
|
||||
}),
|
||||
},
|
||||
backgroundWork: {
|
||||
stop: (item, options) => daemonClient.stopAgentBackgroundWork(id, item, options),
|
||||
list: (options) => daemonClient.listAgentBackgroundWork(id, options),
|
||||
subscribe: (listener, options) => {
|
||||
const observation = observeBackgroundWork();
|
||||
// Bumped by every delivery so a list answered after a newer update is dropped.
|
||||
let sequence = 0;
|
||||
let released = false;
|
||||
const reportError = (error: unknown) => {
|
||||
if (released) return;
|
||||
if (options?.onError) options.onError(error);
|
||||
else console.error("Background work subscription failed", error);
|
||||
};
|
||||
observation.subscribe({
|
||||
error: reportError,
|
||||
snapshot: () => {
|
||||
const listed = ++sequence;
|
||||
void daemonClient
|
||||
.listAgentBackgroundWork(id)
|
||||
.then((items) => {
|
||||
if (!released && listed === sequence) listener(items);
|
||||
return undefined;
|
||||
})
|
||||
.catch(reportError);
|
||||
},
|
||||
update: (message) => {
|
||||
if (message.type !== "agent.background_work.update") return;
|
||||
if (message.payload.agentId !== id) return;
|
||||
sequence++;
|
||||
listener(message.payload.items);
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
released = true;
|
||||
void observation
|
||||
.release()
|
||||
.catch((error) =>
|
||||
console.error("Background work subscription cleanup failed", error),
|
||||
);
|
||||
};
|
||||
},
|
||||
},
|
||||
get workspaceId() {
|
||||
return current?.workspaceId ?? null;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user