Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .oxfmtrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
// the exact indentation the rule tests assert on.
// `packages/*/npm` is generated dnt output, written by a test while the
// battery runs; format-checking it fails on a file nobody wrote by hand.
"ignorePatterns": ["**/*.md", "scripts/tests/fixtures/**", "**/npm/**"]
"ignorePatterns": ["**/*.md", "scripts/tests/fixtures/**", "**/npm/**", "**/vendor/**"]
}
13 changes: 8 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,14 @@ here:
root `deno.json`. Its `exclude` list holds the paths that must stay
unchecked: the deliberately-malformed `scripts/tests/fixtures`; `.xmd-eval`,
where a running document writes the `.ts` files its eval blocks compile to;
and `**/npm`, the dnt build's output, which a test rewrites while the battery
runs. All three are generated and belong to whichever command is producing
them — type-checking one mid-write fails on a partial file, and fails the
whole workspace check for a file nobody committed. The same output is skipped
by `lint` and `fmt`, for the same reason.
`**/npm`, the dnt build's output, which a test rewrites while the battery
runs; ignored local `.claude/worktrees`; ignored generated spike vendor
builds; and byte-identical vendored TypeScript inputs whose deterministic
JavaScript and declaration output is checked instead. Generated paths belong
to whichever command is producing them — type-checking one mid-write fails on
a partial file, and fails the whole workspace check for a file nobody
committed. The corresponding generated output is skipped by `lint` and
`fmt`, for the same reason.
- `test:node` and `test:bun` derive the same corpus through
`scripts/lib/test-files.ts`, which walks `tests/` beneath each workspace
member plus `scripts/tests/` — that boundary, and nothing else. A new
Expand Down
40 changes: 38 additions & 2 deletions architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,13 @@ before their parent's effect begins. Declarative Git operations, including
staging, switching and committing, operate on the same transactional Workspace
rather than invoking an untracked native Git side effect.

The retained-filesystem foundation supplies this boundary to provider-level
Workspace effects. A live durable-effect coordinator lets the provider run the
mutation in an operation savepoint, publish or reuse an immutable root, and
invoke the existing filtered-event continuation while the caller-owned outer
transaction remains active. The default coordinator keeps ordinary durable
effects unchanged, and replay does not invoke either coordinator.

An external provider cannot join that transaction. Prompt, Git push and pull
request effects derive a stable identity from the run and expansion, ask the
provider to perform or reconcile that identity, then append one local result
Expand Down Expand Up @@ -371,6 +378,34 @@ host serializes its Workspace-local effect transactions. A second long-lived
DOFS connection is not a coherent reader because provider caches may retain
negative entries across another connection's commit.

Each provider registry entry owns the physical connection, the single DOFS
database wrapper, the Workspace filesystem, the cooperative turn queue and the
savepoint allocator. Scope-owned database handles are leases over that entry.
DOFS synchronous transactions are nested savepoints on the caller-owned outer
transaction; they never open a second connection or a top-level transaction.

Schema version 1 is the complete pre-release schema. Its exact structural
manifest freezes the workflow tables together with the pinned DOFS
schema-version-5 objects and the Workspace root tables. Initialization happens
only for an empty database, creates the canonical empty root and current-root
pointer in the same immediate transaction, and never repairs an existing file.
The earlier metadata-only version-1 shape is unsupported and is refused
unchanged.

Workspace root format 1 is fixed-key-order canonical UTF-8 JSON over the root
directory and every reachable canonical absolute POSIX path. UTF-8 byte order
defines path ordering without Unicode normalization. Entries retain topology,
kind, mode, observable mtime, symlink target, file size and immutable DOFS
manifest identity; deterministic path-order groups represent hardlinks without
hashing mutable inode identity. The root ID is lowercase SHA-256 over the
domain-separated canonical bytes.

Root rows retain exact normalized references to their transitive DOFS manifests
and blobs. Foreign keys keep that content alive, and this foundation neither
exposes nor invokes DOFS garbage collection. An adapter-private materializer
rebuilds a complete live DOFS frontier from a retained root inside the caller's
transaction and verifies that resnapshotting produces the same root ID.

The initial topology requires neither writable FUSE nor native subprocess
access and does not bundle `workerd`. A Cloudflare-hosted or workerd-backed
provider may install the same contextual contract without changing documents or
Expand Down Expand Up @@ -638,11 +673,12 @@ Status is measured against main.
| workflow run storage | creates or compatibly finds one run by public run ID, and retains its identity, state, document executions and filtered journal | built on main |
| caller-owned storage transaction | publishes several changes, including journal events, in one transaction nothing else enlists in | built on main |
| `xmd workflow start` / `xmd workflow resume` | starts or resumes a workflow run from the CLI | defined in `specs/workflow-workspace-spec.md`, unbuilt; the lookup it resumes through is built |
| implicit workflow Workspace | retains provider-neutral filesystem, repository and attachment state by run ID | defined in `specs/workflow-workspace-spec.md`, unbuilt (#218) |
| retained Workspace filesystem foundation | retains immutable, restorable filesystem roots and publishes provider-level mutations with filtered journal results | built by #365; no public workflow or `<File>` surface |
| implicit workflow Workspace | retains provider-neutral repository, process and attachment state by run ID | filesystem foundation built by #365; public composition remains unbuilt (#218) |
| Repository / Worktree / transactional Git effects | compose named checkouts and publish local mutations with their journal result | defined in `specs/workflow-workspace-spec.md`, unbuilt |
| workflow inspection and history fork | reads status/history without advancing a run and creates a new run from a checkpoint | defined in `specs/workflow-workspace-spec.md`, unbuilt |
| read-only workflow Agent / generated XMD | lets an Agent inspect a derived view and propose constrained executable changes | defined in `specs/workflow-workspace-spec.md`, unbuilt |
| Deno-local DOFS provider | stores the authoritative local Workspace in SQLite | persistence POC complete; effect-transaction integration unbuilt |
| Deno-local DOFS provider | stores the authoritative local Workspace in SQLite | retained filesystem and atomic transaction foundation built by #365 |
| scoped Worker Shell | executes `just-bash` through the Workspace adapter inside a Deno Worker | containment and effect-transaction POCs complete (#351, #357); production integration unbuilt |
| `<Retry max timeout>` | retry a region until it completes | defined, unbuilt |
| suspension effect | suspend durably | defined, unbuilt |
Expand Down
11 changes: 10 additions & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
{
"workspace": ["packages/*", "site"],
"exclude": ["scripts/tests/fixtures", ".xmd-eval", "**/npm"],
"exclude": [
"scripts/tests/fixtures",
".xmd-eval",
".claude/worktrees",
"**/npm",
"spikes/*/vendor/**/dist",
"**/vendor/**/upstream",
"**/vendor/**/generated/**/*.d.ts"
],
"nodeModulesDir": "auto",
"lock": {
"frozen": true
Expand Down Expand Up @@ -52,6 +60,7 @@
"bump": "deno run -A scripts/bump-version.ts",
"test": "deno test --allow-all --frozen",
"verify": "deno run --allow-all --node-modules-dir=none --cached-only --frozen scripts/preflight.ts scripts/verify.ts",
"vendor:verify": "deno run --allow-read --allow-write=/tmp --allow-env --allow-run --cached-only --frozen scripts/verify-cloudflare-dofs.ts",
"check": "deno check --frozen",
"check:jsr": "deno publish --dry-run --allow-dirty",
"review": "deno run --allow-all packages/cli/src/deno.ts run .reviews/ReviewPR.md --component-dir .reviews/components --component-dir .reviews/policies --component-dir packages/core/components -j .reviews/journal.jsonl",
Expand Down
3 changes: 2 additions & 1 deletion deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"test:node": "tsx scripts/runtime-tests.ts node",
"test:bun": "bun scripts/runtime-tests.ts bun",
"test:deno": "deno task test",
"lint": "oxlint -c .oxlintrc.json --ignore-pattern 'scripts/tests/fixtures/**' --ignore-pattern '**/npm/**' packages scripts && oxfmt --check packages scripts",
"lint": "oxlint -c .oxlintrc.json --ignore-pattern 'scripts/tests/fixtures/**' --ignore-pattern '**/npm/**' --ignore-pattern '**/vendor/**' packages scripts && oxfmt --check packages scripts",
"fmt": "oxfmt --write packages scripts"
},
"workspaces": [
Expand Down
36 changes: 18 additions & 18 deletions packages/durable-streams/effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ import type { Operation } from "effection";
import { type DurableContext, DurableCtx } from "./context.ts";
import { Divergence } from "./divergence.ts";
import { StaleInputError } from "./errors.ts";
import {
coordinateLiveDurableEffect,
type LiveDurableEffectCoordinate,
} from "./live-coordinator.ts";
import { ReplayGuard } from "./replay-guard.ts";
import { protocolToEffection, serializeError } from "./serialize.ts";
import type {
Expand Down Expand Up @@ -289,14 +293,15 @@ export function createDurableEffect<T>(
* concurrency — if the scope tears down, the operation is cancelled.
*
* Use this for durableCall and any effect where the work is expressed
* as an Operation (or can be wrapped as one via Effection's call()).
* as an Operation (or can be wrapped as one via Effection's until()).
*
* @param desc Structured description for the journal and divergence detection
* @param execute Returns an Operation to run during live execution
*/
export function createDurableOperation<T extends Json>(
desc: EffectDescription,
execute: () => Operation<T>,
coordinate: LiveDurableEffectCoordinate = coordinateLiveDurableEffect,
): DurableEffect<T> {
return {
description: `${desc.type}(${desc.name})`,
Expand All @@ -316,24 +321,19 @@ export function createDurableOperation<T extends Json>(
// Run the entire execute → capture → persist → resolve sequence
// as a structured operation in the routine's scope.
routine.scope.run(function* () {
let result: Result;
try {
const value = yield* execute();
result = { status: "ok", value: value as Json };
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
result = { status: "err", error: serializeError(error) };
}

const event: Yield = {
type: "yield",
coroutineId: ctx.coroutineId,
description: desc,
result,
};

try {
yield* ctx.stream.append(event);
const result = yield* coordinate({
execute,
*publish(result): Operation<void> {
const event: Yield = {
type: "yield",
coroutineId: ctx.coroutineId,
description: desc,
result,
};
yield* ctx.stream.append(event);
},
});
resolve(protocolToEffection<T>(result));
} catch (err) {
resolve({
Expand Down
34 changes: 34 additions & 0 deletions packages/durable-streams/live-coordinator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { type Api, createApi } from "@effectionx/context-api";
import type { Operation } from "effection";
import { serializeError } from "./serialize.ts";
import type { Json, Result } from "./types.ts";

export interface LiveDurableEffect<T extends Json> {
execute(): Operation<T>;
publish(result: Result): Operation<void>;
}

export interface LiveDurableEffectCoordinatorApi {
coordinate(effect: LiveDurableEffect<Json>): Operation<Result>;
}

export type LiveDurableEffectCoordinate = <T extends Json>(
effect: LiveDurableEffect<T>,
) => Operation<Result>;

export const LiveDurableEffectCoordinator: Api<LiveDurableEffectCoordinatorApi> =
createApi<LiveDurableEffectCoordinatorApi>("executablemd.durable-streams.live-coordinator", {
*coordinate(effect: LiveDurableEffect<Json>): Operation<Result> {
let result: Result;
try {
result = { status: "ok", value: yield* effect.execute() };
} catch (error) {
const failure = error instanceof Error ? error : new Error(String(error));
result = { status: "err", error: serializeError(failure) };
}
yield* effect.publish(result);
return result;
},
});

export const coordinateLiveDurableEffect = LiveDurableEffectCoordinator.operations.coordinate;
6 changes: 6 additions & 0 deletions packages/durable-streams/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ export { parseDurableEvent } from "./parse.ts";
// Core effect factories
export { createDurableEffect, createDurableOperation } from "./effect.ts";
export type { Executor } from "./effect.ts";
export { LiveDurableEffectCoordinator } from "./live-coordinator.ts";
export type {
LiveDurableEffect,
LiveDurableEffectCoordinate,
LiveDurableEffectCoordinatorApi,
} from "./live-coordinator.ts";

// Workflow-enabled effects
export { durableAction, durableCall, durableSleep, versionCheck } from "./operations.ts";
Expand Down
72 changes: 72 additions & 0 deletions packages/durable-streams/tests/live-coordinator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { describe, it } from "@executablemd/test-support/bdd";
import { expect } from "@executablemd/test-support/expect";
import type { Operation } from "effection";
import {
createDurableOperation,
durableRun,
InMemoryStream,
type Json,
type LiveDurableEffect,
type Result,
type Workflow,
} from "../mod.ts";

describe("live durable effect coordinator", () => {
it("preserves the default live publication and replay behavior", function* () {
const stream = new InMemoryStream();
let executions = 0;

function* workflow(): Workflow<Json> {
yield createDurableOperation<Json>(
{ type: "coordinator", name: "default" },
function* (): Operation<Json> {
executions += 1;
return "live";
},
);
return "live";
}

expect(yield* durableRun(workflow, { stream })).toBe("live");
expect(executions).toBe(1);
expect(stream.snapshot().filter((event) => event.type === "yield")).toHaveLength(1);

expect(
yield* durableRun(workflow, {
stream: new InMemoryStream(stream.snapshot()),
}),
).toBe("live");
expect(executions).toBe(1);
});

it("lets a provider coordinate execution and publication without changing the protocol", function* () {
const stream = new InMemoryStream();
const order: string[] = [];

function* coordinate<T extends Json>(effect: LiveDurableEffect<T>): Operation<Result> {
order.push("begin");
const value = yield* effect.execute();
const result: Result = { status: "ok", value };
order.push("publish");
yield* effect.publish(result);
order.push("commit");
return result;
}

function* workflow(): Workflow<Json> {
yield createDurableOperation<Json>(
{ type: "coordinator", name: "provider" },
function* (): Operation<Json> {
order.push("execute");
return { retained: true };
},
coordinate,
);
return { retained: true };
}

expect(yield* durableRun(workflow, { stream })).toEqual({ retained: true });
expect(order).toEqual(["begin", "execute", "publish", "commit"]);
expect(stream.snapshot().filter((event) => event.type === "yield")).toHaveLength(1);
});
});
3 changes: 3 additions & 0 deletions packages/workflow/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@
"exports": {
".": "./mod.ts",
"./deno": "./deno.ts"
},
"publish": {
"exclude": ["!vendor/cloudflare-computer-dofs/generated/**/*.d.ts"]
}
}
7 changes: 7 additions & 0 deletions packages/workflow/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ export type {
WorkflowRunStorageApi,
WorkflowRunTransaction,
} from "./src/storage/api.ts";
export type {
WorkflowWorkspace,
WorkspaceDirectoryEntry,
WorkspaceFilesystem,
WorkspaceStat,
} from "./src/workspace/api.ts";

export { definitionToJson, parseWorkflowDefinition } from "./src/storage/definition.ts";
export type { GitWorkflowDefinitionV1, WorkflowDefinition } from "./src/storage/definition.ts";
Expand Down Expand Up @@ -64,6 +70,7 @@ export {
WorkflowDatabaseClosedError,
WorkflowDatabaseCorruptError,
WorkflowDatabaseFormatError,
WorkflowIncompleteVersionOneError,
WorkflowDefinitionError,
WorkflowDocumentExecutionError,
WorkflowRecordMalformedError,
Expand Down
3 changes: 2 additions & 1 deletion packages/workflow/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@executablemd/core": "workspace:*",
"@executablemd/durable-streams": "workspace:*",
"@executablemd/runtime": "workspace:*",
"effection": "4.1.0"
"effection": "4.1.0",
"zod": "^4.3.6"
}
}
Loading
Loading