Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ec92fdf
feat(harness): carry an offset on live text and thinking deltas
sanchitrk Nov 5, 2026
23f3ba7
feat(harness): durable run lifecycle, stored run state and startup sweep
sanchitrk Nov 5, 2026
10bd785
feat(harness): store-wide revision and tombstones
sanchitrk Nov 5, 2026
ef46b1b
Merge branch 'feat/harness-revision' into feat/harness-sync
sanchitrk Nov 5, 2026
2b3bbad
Merge branch 'feat/harness-run-lifecycle' into feat/harness-sync
sanchitrk Nov 5, 2026
6e67b8e
Merge branch 'feat/harness-delta-offset' into feat/harness-sync
sanchitrk Nov 5, 2026
0e0654b
fix(harness): stamp rev on run-state projections
sanchitrk Nov 5, 2026
b0e4ae7
fix(harness): end an abandoned run with its failure only
sanchitrk Nov 5, 2026
cb6788d
feat(harness): read the index by revision, with fork cuts and full se…
sanchitrk Nov 5, 2026
c7249f0
feat(codework): store identity on server.info and the index.sync stream
sanchitrk Nov 5, 2026
e6dec02
feat(harness): read a thread's line by page and its detail by revision
sanchitrk Nov 5, 2026
e191ab5
feat(codework): the thread.sync stream
sanchitrk Nov 5, 2026
8bf2b38
feat(codework): thread.page reads older pages of a thread's line
sanchitrk Nov 5, 2026
610baeb
feat(server): create the sync tables and keep servers in the machine …
sanchitrk Nov 5, 2026
8311181
feat(widget): mirror the CodeWork sync streams and store identity
sanchitrk Nov 5, 2026
afc0e3c
feat(server): sync every machine's CodeWork into app.db
sanchitrk Nov 5, 2026
3c89e83
feat(server): track threads with user-set status, priority, groups, o…
sanchitrk Nov 5, 2026
92316c1
feat(codework): carry a queued prompt's admittedSeq on the wire
sanchitrk Nov 5, 2026
1e75577
fix(server): store pending inbox prompts and undelete rows that come …
sanchitrk Nov 5, 2026
b9ee11a
Merge branch 'fix/sync-inbox' into feat/server-tracking
sanchitrk Nov 5, 2026
862f1d9
test(server): sync across OrbStack Linux machines
sanchitrk Nov 5, 2026
54baf80
test(server): time the stop/start shuffle step by its resume only
sanchitrk Nov 5, 2026
a094d15
feat(web): read the sidebar and session threads from the synced tables
sanchitrk Nov 5, 2026
3aa665c
fix(web): spin a fork's tab for its own run, start parts from the fir…
sanchitrk Nov 5, 2026
70379e5
feat(server): add the dashboard read over the desktop columns
sanchitrk Nov 5, 2026
5d8764b
feat(web): add the dashboard widget and pending and stopping run states
sanchitrk Nov 5, 2026
d19608e
fix(web): fail a follow at once for a thread its machine does not have
sanchitrk Nov 5, 2026
7f1b8fe
fix(server): order the sidebar by thread activity and leave subagents…
sanchitrk Nov 5, 2026
cb1c9cd
fix(web): show a tab's run state as stored and clear a stop once ever…
sanchitrk Nov 5, 2026
1943208
fix(web): track the start page's first prompt as pending
sanchitrk Nov 5, 2026
e4f1aaa
fix(web): drop a thread on a dashboard list's header at the top of th…
sanchitrk Nov 5, 2026
ea6793d
feat(web): fork a session at a message
sanchitrk Nov 5, 2026
e75a395
chore(web): keep useDrawn private to the display module
sanchitrk Nov 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
369 changes: 336 additions & 33 deletions apps/contract/src/contract.ts

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"effect": "4.0.5"
},
"devDependencies": {
"@codeworksh/aikit": "workspace:*",
"@types/node": "catalog:",
"typescript": "catalog:",
"vite-plus": "catalog:"
Expand Down
7 changes: 4 additions & 3 deletions apps/server/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const found = Effect.gen(function* () {
});

/** The `codework` a server runs: the one chosen for it, else the one found. */
const executableOf = (row: Servers.Row, foundOnce: typeof found) => {
const executableOf = (row: Servers.LocalRow, foundOnce: typeof found) => {
const { executable } = row;
if (executable === null) return foundOnce;
const command = split(executable);
Expand Down Expand Up @@ -151,14 +151,15 @@ export const awaitConnection = Effect.gen(function* () {
);
});

/** Every server and how it is set up, with its executable and process as found now. */
/** Every server and how it is set up, with a local one's executable and process as found now. */
export const statuses = Effect.gen(function* () {
const servers = yield* Servers.Service;
const foundOnce = yield* Effect.cached(found);
return yield* Effect.forEach(
yield* servers.list,
(row) =>
Effect.gen(function* () {
if (row.kind === "remote") return row satisfies ServerStatus;
const home = servers.homeOf(row);
const [cli, server] = yield* Effect.all(
[executableOf(row, foundOnce), serverAt(home.path, row.isDefault)],
Expand Down Expand Up @@ -186,7 +187,7 @@ export const statuses = Effect.gen(function* () {

// The CLI owns a server's lifecycle; the app only asks it, with the server's variables and in its
// home, which a variable never overrides.
export const service = Effect.fn("Cli.service")(function* (row: Servers.Row, action: CliAction) {
export const service = Effect.fn("Cli.service")(function* (row: Servers.LocalRow, action: CliAction) {
const servers = yield* Servers.Service;
const cli = yield* executableOf(row, found);
if (cli === null || !("version" in cli))
Expand Down
85 changes: 56 additions & 29 deletions apps/server/src/codework.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import { Codework as Mirror } from "@codeworksh/widget/codework";
import { Codework as Client } from "@codeworksh/widget/backend";
import { NodeSocket } from "@effect/platform-node";
import { Deferred, Duration, Effect, FileSystem, Layer, Schedule, Schema, Stream } from "effect";
import { Deferred, Duration, Effect, FileSystem, Layer, Option, Schedule, Schema, Stream } from "effect";
import { RpcClient, RpcSerialization } from "effect/rpc";
import { Socket } from "effect/socket";
/* @effect-diagnostics nodeBuiltinImport:off -- a home's registration is a file path. */
import path from "node:path";
import { CodeworkUnavailable, HostError } from "@codeworksh/contract";
import { Servers } from "./servers.ts";

export type ThreadInfo = Mirror.ThreadInfo;

/** What an untitled thread is called until its first prompt names it. */
export const UNTITLED = Mirror.UNTITLED;

/** How a running server advertises itself: `<home>/service.json`, written by `codework serve`. */
const Registration = Schema.fromJsonString(
Schema.Struct({ pid: Schema.Int, version: Schema.String, url: Schema.String, port: Schema.Int }),
Expand Down Expand Up @@ -62,37 +57,69 @@ const endOnHomeChange = (socket: Socket.Socket, changes: Stream.Stream<string>):
}),
});

/** A client over `socket`, which reconnects on its own. */
const connect = <R>(socket: Layer.Layer<Socket.Socket, never, R>) =>
Layer.effect(Client, RpcClient.make(Mirror.Api, { flatten: true })).pipe(
Layer.provide(RpcClient.layerProtocolSocket({ retryTransientErrors: true })),
Layer.provide(socket.pipe(Layer.provide(NodeSocket.layerWebSocketConstructor))),
Layer.provide(RpcSerialization.layerNdjson),
Layer.orDie,
);

/**
* The app's one connection to a CodeWork server: the default server's. The registration is read on
* every connect, so a server restarted on a new port, a new home or a new default is picked up
* without restarting this app; until one is registered, connecting waits.
* The URL `registered` reads, waited for until there is one. The socket reads its URL while opening,
* which Effect runs uninterruptibly; waiting there for a server to register would otherwise keep the
* app server from ever shutting down.
*/
export const layer = Layer.effect(Client, RpcClient.make(Mirror.Api, { flatten: true })).pipe(
Layer.provide(RpcClient.layerProtocolSocket({ retryTransientErrors: true })),
Layer.provide(
const waitFor = (registered: Effect.Effect<Option.Option<{ readonly url: string }>>) =>
registered.pipe(
Effect.flatMap(Effect.fromOption),
Effect.map((found) => found.url),
Effect.retry(Schedule.spaced("1 second")),
Effect.orDie,
Effect.interruptible,
);

/**
* The app's connection to the default server. The registration is read on every connect, so a server
* restarted on a new port, a new home or a new default is picked up without restarting this app;
* until one is registered, connecting waits.
*/
export const layer = connect(
Layer.effect(
Socket.Socket,
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const servers = yield* Servers.Service;
const url = waitFor(
registration.pipe(
Effect.provideService(FileSystem.FileSystem, fs),
Effect.provideService(Servers.Service, servers),
),
);
return endOnHomeChange(yield* Socket.makeWebSocket(url), servers.homeChanges);
}),
),
);

/**
* A connection to one machine's server: a remote one at its URL, a local one at what its home
* registers, read on every connect as {@link layer} does.
*/
export const machine = (row: Servers.Row) =>
connect(
Layer.effect(
Socket.Socket,
Effect.gen(function* () {
if (row.kind === "remote") return yield* Socket.makeWebSocket(row.url);
const fs = yield* FileSystem.FileSystem;
const servers = yield* Servers.Service;
const url = registration.pipe(
Effect.flatMap(Effect.fromOption),
Effect.map((registered) => registered.url),
Effect.retry(Schedule.spaced("1 second")),
Effect.orDie,
Effect.provideService(FileSystem.FileSystem, fs),
Effect.provideService(Servers.Service, servers),
// The socket reads its URL while opening, which Effect runs uninterruptibly; waiting there
// for a server to register would otherwise keep the app server from ever shutting down.
Effect.interruptible,
const home = (yield* Servers.Service).homeOf(row).path;
return yield* Socket.makeWebSocket(
waitFor(registrationAt(home).pipe(Effect.provideService(FileSystem.FileSystem, fs))),
);
return endOnHomeChange(yield* Socket.makeWebSocket(url), servers.homeChanges);
}),
).pipe(Layer.provide(NodeSocket.layerWebSocketConstructor)),
),
Layer.provide(RpcSerialization.layerNdjson),
Layer.orDie,
);
),
);

export interface CallOptions {
/** How long the server has to answer; 5 seconds unless given. */
Expand Down
Loading
Loading