Files
paseo/packages/client/src/timeline-subscription/index.ts
T
Mohamed Boudra 03a54a171d Keep older daemons usable and let clients choose timeline recovery (#4737)
* fix(subscriptions): preserve older-host workflows and explicit timeline recovery

Keep established app and SDK workflows available on older daemons without emulating independent directory filters. Restore live timeline membership without choosing the consumer's history recovery policy. Preserve daemon filesystem paths separately from legacy workspace IDs.

* fix(client): keep legacy workspace replies scoped to the requested page
2026-09-12 08:50:16 +02:00

76 lines
2.3 KiB
TypeScript

import type { SessionOutboundMessage } from "@getpaseo/protocol/messages";
import type { OwnedSubscription, TimelineSubscription } from "../connection/index.js";
type TimelineUpdate = Extract<
SessionOutboundMessage,
{ type: "agent_stream" | "agent.timeline.replacement" }
>;
/** Local SDK lifecycle messages; these are not additional wire RPCs. */
export type TimelineMessage =
| TimelineUpdate
| {
type: "agent.timeline.subscription_restored";
payload: { agentId: string; subscriptionId: string };
}
| { type: "agent.timeline.error"; payload: { agentId: string; error: string } };
/** Restores live delivery; the consumer owns history reads and gap recovery. */
export function subscribeTimeline(
agentId: string,
observation: OwnedSubscription<{ agentIds: string[] }>,
handler: (message: TimelineMessage) => void,
reportError: (error: unknown) => void,
): TimelineSubscription {
let established = false;
let released = false;
const notify = (message: TimelineMessage) => {
if (released) return;
try {
handler(message);
} catch (error) {
reportError(error);
}
};
const release = () => {
released = true;
return observation.release();
};
const stop = () => {
// Use the public release entry point so the owning API also forgets this handle.
void subscription.release().catch(reportError);
};
const fail = (error: unknown) => {
notify({
type: "agent.timeline.error",
payload: { agentId, error: error instanceof Error ? error.message : String(error) },
});
stop();
};
observation.subscribe({
snapshot: ({ subscriptionId }) => {
if (!established) {
established = true;
return;
}
notify({
type: "agent.timeline.subscription_restored",
payload: { agentId, subscriptionId },
});
},
update: (message) => {
if (message.type === "agent_stream" || message.type === "agent.timeline.replacement")
notify(message);
},
error: fail,
});
const subscription: TimelineSubscription = Object.assign(stop, {
ready: observation.ready.then(() => undefined),
release,
subscriptionId: null,
});
Object.defineProperty(subscription, "subscriptionId", { get: () => observation.subscriptionId });
void subscription.ready.catch(fail);
return subscription;
}