- Archive dormant apps (daemon, desktop, native, tui) and superseded packages/server into repos/ for reference - Archive 21 dead web components (chat page shell, work-os cluster, strays) into repos/web/; keep reused message-rendering primitives in components/chat - Merge @code/work-os into @code/primitives; work.ts absorbed via ./work subpath and barrel export; update all four importers - Add docs/futures.md tracking audio/video (blocked at Flue + provider), web layout cleanup, and message rendering quality - Repoint dev:zopu script to @code/agents (the live Flue server) - Note repos/ reference-code convention in AGENTS.md
175 lines
5.6 KiB
TypeScript
175 lines
5.6 KiB
TypeScript
import { api } from "@code/backend/convex/_generated/api";
|
|
import type { Id } from "@code/backend/convex/_generated/dataModel";
|
|
import { env } from "@code/env/server";
|
|
import { ConvexClient } from "convex/browser";
|
|
import { Cause, Context, Effect, Layer, Queue, Schema, Stream } from "effect";
|
|
|
|
export class ConvexControlPlaneError extends Schema.TaggedErrorClass<ConvexControlPlaneError>()(
|
|
"ConvexControlPlaneError",
|
|
{
|
|
operation: Schema.String,
|
|
cause: Schema.Defect(),
|
|
}
|
|
) {}
|
|
|
|
export interface DaemonCommand {
|
|
readonly _id: Id<"daemonCommands">;
|
|
readonly daemonId: string;
|
|
readonly actorKey: readonly string[];
|
|
readonly method: string;
|
|
readonly args: unknown;
|
|
}
|
|
|
|
const tryPromise = <A>(operation: string, evaluate: () => Promise<A>) =>
|
|
Effect.tryPromise({
|
|
try: evaluate,
|
|
catch: (cause) => new ConvexControlPlaneError({ operation, cause }),
|
|
});
|
|
|
|
export class ConvexControlPlane extends Context.Service<
|
|
ConvexControlPlane,
|
|
{
|
|
readonly commands: Stream.Stream<
|
|
readonly DaemonCommand[],
|
|
ConvexControlPlaneError
|
|
>;
|
|
readonly connect: (input: {
|
|
readonly sessionId: string;
|
|
readonly hostname: string;
|
|
readonly platform: string;
|
|
readonly architecture: string;
|
|
}) => Effect.Effect<void, ConvexControlPlaneError>;
|
|
readonly heartbeat: (
|
|
sessionId: string
|
|
) => Effect.Effect<void, ConvexControlPlaneError>;
|
|
readonly disconnect: (
|
|
sessionId: string
|
|
) => Effect.Effect<void, ConvexControlPlaneError>;
|
|
readonly claim: (
|
|
commandId: Id<"daemonCommands">,
|
|
sessionId: string
|
|
) => Effect.Effect<DaemonCommand | null, ConvexControlPlaneError>;
|
|
readonly start: (
|
|
commandId: Id<"daemonCommands">,
|
|
sessionId: string
|
|
) => Effect.Effect<boolean, ConvexControlPlaneError>;
|
|
readonly succeed: (
|
|
commandId: Id<"daemonCommands">,
|
|
sessionId: string,
|
|
result: unknown
|
|
) => Effect.Effect<boolean, ConvexControlPlaneError>;
|
|
readonly fail: (
|
|
commandId: Id<"daemonCommands">,
|
|
sessionId: string,
|
|
error: string
|
|
) => Effect.Effect<boolean, ConvexControlPlaneError>;
|
|
readonly recordEvent: (input: {
|
|
readonly commandId?: Id<"daemonCommands">;
|
|
readonly kind: string;
|
|
readonly data: unknown;
|
|
}) => Effect.Effect<void, ConvexControlPlaneError>;
|
|
readonly close: Effect.Effect<void, ConvexControlPlaneError>;
|
|
}
|
|
>()("@code/daemon/ConvexControlPlane") {
|
|
static readonly layer = Layer.effect(
|
|
ConvexControlPlane,
|
|
Effect.gen(function* () {
|
|
const client = yield* Effect.acquireRelease(
|
|
Effect.sync(() => new ConvexClient(env.CONVEX_URL)),
|
|
(client) => Effect.promise(() => client.close())
|
|
);
|
|
const commands = Stream.callback<
|
|
readonly DaemonCommand[],
|
|
ConvexControlPlaneError
|
|
>((queue) =>
|
|
Effect.acquireRelease(
|
|
Effect.sync(() =>
|
|
client.onUpdate(
|
|
api.daemonCommands.available,
|
|
{ daemonId: env.DAEMON_ID },
|
|
(items) => {
|
|
Queue.offerUnsafe(queue, items);
|
|
},
|
|
(cause) => {
|
|
Queue.failCauseUnsafe(
|
|
queue,
|
|
Cause.fail(
|
|
new ConvexControlPlaneError({
|
|
operation: "subscribe commands",
|
|
cause,
|
|
})
|
|
)
|
|
);
|
|
}
|
|
)
|
|
),
|
|
(unsubscribe) => Effect.sync(unsubscribe)
|
|
)
|
|
);
|
|
|
|
return ConvexControlPlane.of({
|
|
commands,
|
|
connect: (input) =>
|
|
tryPromise("connect daemon", async () => {
|
|
await client.mutation(api.daemonRuntime.connect, {
|
|
daemonId: env.DAEMON_ID,
|
|
version: env.DAEMON_VERSION,
|
|
...input,
|
|
});
|
|
}),
|
|
heartbeat: (sessionId) =>
|
|
tryPromise("heartbeat daemon", async () => {
|
|
await client.mutation(api.daemonRuntime.heartbeat, {
|
|
daemonId: env.DAEMON_ID,
|
|
sessionId,
|
|
});
|
|
}),
|
|
disconnect: (sessionId) =>
|
|
tryPromise("disconnect daemon", async () => {
|
|
await client.mutation(api.daemonRuntime.disconnect, {
|
|
daemonId: env.DAEMON_ID,
|
|
sessionId,
|
|
});
|
|
}),
|
|
claim: (commandId, sessionId) =>
|
|
tryPromise("claim command", () =>
|
|
client.mutation(api.daemonCommands.claim, {
|
|
commandId,
|
|
daemonId: env.DAEMON_ID,
|
|
sessionId,
|
|
leaseDurationMs: env.DAEMON_COMMAND_LEASE_MS,
|
|
})
|
|
),
|
|
start: (commandId, sessionId) =>
|
|
tryPromise("start command", () =>
|
|
client.mutation(api.daemonCommands.start, { commandId, sessionId })
|
|
),
|
|
succeed: (commandId, sessionId, result) =>
|
|
tryPromise("complete command", () =>
|
|
client.mutation(api.daemonCommands.succeed, {
|
|
commandId,
|
|
sessionId,
|
|
result,
|
|
})
|
|
),
|
|
fail: (commandId, sessionId, error) =>
|
|
tryPromise("fail command", () =>
|
|
client.mutation(api.daemonCommands.fail, {
|
|
commandId,
|
|
sessionId,
|
|
error,
|
|
})
|
|
),
|
|
recordEvent: (input) =>
|
|
tryPromise("record daemon event", async () => {
|
|
await client.mutation(api.daemonRuntime.recordEvent, {
|
|
daemonId: env.DAEMON_ID,
|
|
...input,
|
|
});
|
|
}),
|
|
close: tryPromise("close Convex client", () => client.close()),
|
|
});
|
|
})
|
|
);
|
|
}
|