Skip to content
Merged
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
105 changes: 102 additions & 3 deletions apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
// @effect-diagnostics nodeBuiltinImport:off
import * as NodeFSP from "node:fs/promises";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";

import {
CheckpointRef,
CommandId,
Expand All @@ -11,6 +16,7 @@ import {
} from "@t3tools/contracts";
import * as NodeServices from "@effect/platform-node/NodeServices";
import * as Effect from "effect/Effect";
import * as Fiber from "effect/Fiber";
import * as Layer from "effect/Layer";
import * as ManagedRuntime from "effect/ManagedRuntime";
import * as Metric from "effect/Metric";
Expand All @@ -22,7 +28,10 @@ import { describe, expect, it } from "vite-plus/test";
import { PersistenceSqlError } from "../../persistence/Errors.ts";
import { OrchestrationCommandReceiptRepositoryLive } from "../../persistence/Layers/OrchestrationCommandReceipts.ts";
import { OrchestrationEventStoreLive } from "../../persistence/Layers/OrchestrationEventStore.ts";
import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts";
import {
makeSqlitePersistenceLive,
SqlitePersistenceMemory,
} from "../../persistence/Layers/Sqlite.ts";
import {
OrchestrationEventStore,
type OrchestrationEventStoreShape,
Expand All @@ -44,7 +53,10 @@ const asMessageId = (value: string): MessageId => MessageId.make(value);
const asTurnId = (value: string): TurnId => TurnId.make(value);
const asCheckpointRef = (value: string): CheckpointRef => CheckpointRef.make(value);

async function createOrchestrationSystem() {
async function createOrchestrationSystem(databasePath?: string) {
const persistence = databasePath
? makeSqlitePersistenceLive(databasePath)
: SqlitePersistenceMemory;
const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), {
prefix: "t3-orchestration-engine-test-",
});
Expand All @@ -58,7 +70,7 @@ async function createOrchestrationSystem() {
Layer.provide(OrchestrationEventStoreLive),
Layer.provide(OrchestrationCommandReceiptRepositoryLive),
Layer.provide(RepositoryIdentityResolver.layer),
Layer.provide(SqlitePersistenceMemory),
Layer.provide(persistence),
Layer.provideMerge(ServerConfigLayer),
Layer.provideMerge(NodeServices.layer),
);
Expand Down Expand Up @@ -1131,6 +1143,93 @@ describe("OrchestrationEngine", () => {
await runtime.dispose();
});

it("does not republish another server's turn when a local dispatch fails", async () => {
const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-shared-db-"));
const databasePath = NodePath.join(directory, "state.sqlite");
const serverA = await createOrchestrationSystem(databasePath);
const serverB = await createOrchestrationSystem(databasePath);
const threadId = ThreadId.make("thread-shared");
const createdAt = now();
const sentinelCommandId = CommandId.make("cmd-shared-rename");
try {
await serverA.run(
serverA.engine.dispatch({
type: "project.create",
commandId: CommandId.make("cmd-shared-project-create"),
projectId: asProjectId("project-shared"),
title: "Shared Project",
workspaceRoot: "/tmp/project-shared",
createdAt,
}),
);
await serverA.run(
serverA.engine.dispatch({
type: "thread.create",
commandId: CommandId.make("cmd-shared-thread-create"),
threadId,
projectId: asProjectId("project-shared"),
title: "shared",
modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5-codex" },
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "approval-required",
branch: null,
worktreePath: null,
createdAt,
}),
);
await serverA.run(
serverA.engine.dispatch({
type: "thread.turn.start",
commandId: CommandId.make("cmd-shared-turn-start"),
threadId,
message: {
messageId: asMessageId("msg-shared"),
role: "user",
text: "hello",
attachments: [],
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "approval-required",
createdAt,
}),
);

const published = await serverB.run(
Effect.gen(function* () {
// Starting immediately subscribes before B dispatches anything.
const published = yield* Stream.runCollect(
Stream.takeUntil(
serverB.engine.streamDomainEvents,
(event) => event.commandId === sentinelCommandId,
),
).pipe(Effect.forkScoped({ startImmediately: true }));
// B's command model is still empty, so this fails and reconciles.
yield* serverB.engine
.dispatch({
type: "thread.meta.update",
commandId: CommandId.make("cmd-shared-stale-rename"),
threadId,
title: "stale",
})
.pipe(Effect.flip);
yield* serverB.engine.dispatch({
type: "thread.meta.update",
commandId: sentinelCommandId,
threadId,
title: "renamed on B",
});
return yield* Fiber.join(published);
}).pipe(Effect.scoped),
);

expect(Array.from(published).map((event) => event.type)).toEqual(["thread.meta-updated"]);
} finally {
await serverA.dispose();
await serverB.dispose();
await NodeFSP.rm(directory, { recursive: true, force: true });
}
});

it("fails command dispatch when command invariants are violated", async () => {
const system = await createOrchestrationSystem();
const { engine } = system;
Expand Down
9 changes: 8 additions & 1 deletion apps/server/src/orchestration/Layers/OrchestrationEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ const makeOrchestrationEngine = Effect.gen(function* () {

const processEnvelope = (envelope: CommandEnvelope): Effect.Effect<void> => {
const dispatchStartSequence = commandReadModel.snapshotSequence;
// Events this dispatch appended. Reconcile republishes only these: a
// shared state directory can contain events another server already
// handled, and republishing them starts a second provider turn.
const appendedEventIds = new Set<OrchestrationEvent["eventId"]>();
let processingStartedAtMs = 0;
const aggregateRef = commandToAggregateRef(envelope.command);
const baseMetricAttributes = {
Expand All @@ -121,7 +125,9 @@ const makeOrchestrationEngine = Effect.gen(function* () {
commandReadModel = yield* projectEventsOntoReadModel(commandReadModel, persistedEvents);

for (const persistedEvent of persistedEvents) {
yield* PubSub.publish(eventPubSub, persistedEvent);
if (appendedEventIds.has(persistedEvent.eventId)) {
yield* PubSub.publish(eventPubSub, persistedEvent);
}
}
});

Expand Down Expand Up @@ -174,6 +180,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {

for (const nextEvent of eventBases) {
const savedEvent = yield* eventStore.append(nextEvent);
appendedEventIds.add(savedEvent.eventId);
nextCommandReadModel = yield* projectEvent(nextCommandReadModel, savedEvent);
yield* projectionPipeline.projectEvent(savedEvent);
committedEvents.push(savedEvent);
Expand Down
52 changes: 51 additions & 1 deletion apps/server/src/provider/Drivers/ClaudeSkills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,46 @@ it.layer(NodeServices.layer)("discoverClaudeSkills", (it) => {
}),
);

it.effect("recovers colon-bearing descriptions with the rest of the frontmatter intact", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-claude-skills-" });
const configDir = path.join(tempDir, "claude-home");

for (const [description, comment] of [
["Browser automation + AI test authoring via kane-cli: run browser objectives, ...", ""],
['Read C:\\skills\\guide#tag: continue with "quoted".', " # trailing: comment"],
] as const) {
yield* writeSkill(
path.join(configDir, "skills"),
"kane-cli",
[
"---",
"name: frontmatter-alias",
`description: ${description}${comment}`,
"allowed-tools: [Read, Write]",
"disable-model-invocation: yes",
"user-invocable: no",
"---",
].join("\n"),
);

const skills = yield* discoverClaudeSkills({ homePath: configDir }, undefined);

assert.deepEqual(skills, [
{
name: "frontmatter-alias",
path: path.join(configDir, "skills", "kane-cli", "SKILL.md"),
enabled: true,
scope: "user",
description,
},
]);
}
}),
);

it.effect("falls back to the directory name and skips malformed frontmatter", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
Expand All @@ -102,7 +142,17 @@ it.layer(NodeServices.layer)("discoverClaudeSkills", (it) => {
const skillsDir = path.join(configDir, "skills");

yield* writeSkill(skillsDir, "no-frontmatter", "# Just a heading\n");
yield* writeSkill(skillsDir, "broken-yaml", "---\nname: [unclosed\n---\n");
for (const [directoryName, field] of [
["broken-yaml", "name: [unclosed"],
["broken-tools", "allowed-tools: [Read, Write"],
["broken-quoted", 'name: "unclosed: text'],
] as const) {
yield* writeSkill(
skillsDir,
directoryName,
["---", "description: Run: browser objectives.", field, "---"].join("\n"),
);
}
// A stray file (not a directory with SKILL.md) must be skipped.
yield* fs.makeDirectory(skillsDir, { recursive: true });
yield* fs.writeFileString(path.join(skillsDir, "README.md"), "not a skill");
Expand Down
21 changes: 19 additions & 2 deletions apps/server/src/provider/Drivers/ClaudeSkills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,28 @@ function parseSkillFrontmatter(contents: string): SkillFrontmatter {
return { kind: "missing" };
}

const frontmatter = match[1] ?? "";
let parsed: unknown;
try {
parsed = parseYamlDocument(match[1] ?? "");
parsed = parseYamlDocument(frontmatter);
} catch {
return { kind: "malformed" };
// Claude Code accepts plain scalars containing `: `. Repair only those,
// leaving comments and YAML structure for the full-document parser.
const repaired = frontmatter.replace(
/^([\w-]+:[ \t]*)([^\r\n]*)/gm,
(line, prefix: string, value: string) => {
const scalar = value.split(/[ \t]+#/)[0] ?? "";
if (!/:[ \t]/.test(scalar) || /^(?:["'[\]{}|>&*!#%@`]|[-?:](?:[ \t]|$))/.test(scalar)) {
return line;
}
return `${prefix}${JSON.stringify(scalar)}${value.slice(scalar.length)}`;
},
);
try {
parsed = parseYamlDocument(repaired);
} catch {
return { kind: "malformed" };
}
}
if (typeof parsed !== "object" || parsed === null) {
return { kind: "malformed" };
Expand Down
91 changes: 91 additions & 0 deletions apps/server/src/provider/Layers/ClaudeAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,30 @@ describe("ClaudeAdapterLive", () => {
);
});

it.effect("preserves xhigh effort and opens the 1M window for Claude Haiku 5.5", () => {
const harness = makeHarness();
return Effect.gen(function* () {
const adapter = yield* ClaudeAdapter;
yield* adapter.startSession({
threadId: THREAD_ID,
provider: ProviderDriverKind.make("claudeAgent"),
modelSelection: createModelSelection(
ProviderInstanceId.make("claudeAgent"),
"claude-haiku-5-5",
[{ id: "effort", value: "xhigh" }],
),
runtimeMode: "full-access",
});

const createInput = harness.getLastCreateQueryInput();
assert.equal(createInput?.options.effort, "xhigh");
assert.equal(createInput?.options.model, "claude-haiku-5-5[1m]");
}).pipe(
Effect.provideService(Random.Random, makeDeterministicRandomService()),
Effect.provide(harness.layer),
);
});

it.effect("falls back to default effort when unsupported max is requested for Sonnet 4.6", () => {
const harness = makeHarness();
return Effect.gen(function* () {
Expand Down Expand Up @@ -1167,6 +1191,73 @@ describe("ClaudeAdapterLive", () => {
);
});

it.effect("keeps a turn open past the result of a Claude-initiated turn", () => {
const harness = makeHarness();
return Effect.gen(function* () {
const adapter = yield* ClaudeAdapter;
const runtimeEventsFiber = yield* adapter.streamEvents.pipe(
Stream.takeUntil((event) => event.type === "session.exited"),
Stream.runCollect,
Effect.forkChild,
);

const session = yield* adapter.startSession({
threadId: THREAD_ID,
provider: ProviderDriverKind.make("claudeAgent"),
runtimeMode: "full-access",
});
const turn = yield* adapter.sendTurn({
threadId: session.threadId,
input: "/compact",
attachments: [],
});

// Recorded order after a resume: Claude first reports a background task
// the previous process left behind, then runs the queued `/compact`.
harness.query.emit({
type: "result",
subtype: "success",
is_error: false,
num_turns: 0,
origin: { kind: "task-notification" },
session_id: "sdk-session-1",
uuid: "result-task-notification",
} as unknown as SDKMessage);
harness.query.emit({
type: "system",
subtype: "compact_boundary",
compact_metadata: { trigger: "manual", pre_tokens: 959489, post_tokens: 10107 },
session_id: "sdk-session-1",
uuid: "compact-boundary",
} as unknown as SDKMessage);
harness.query.emit({
type: "result",
subtype: "success",
is_error: false,
num_turns: 0,
user_message_uuid: turn.turnId,
user_message_uuids: [turn.turnId],
local_command: "compact",
session_id: "sdk-session-1",
uuid: "result-compact",
} as unknown as SDKMessage);
harness.query.finish();

const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber));
const compactedIndex = runtimeEvents.findIndex(
(event) => event.type === "thread.state.changed" && event.payload.state === "compacted",
);
const completedIndex = runtimeEvents.findIndex((event) => event.type === "turn.completed");
assert.equal(runtimeEvents.filter((event) => event.type === "turn.completed").length, 1);
assert.equal(String(runtimeEvents[completedIndex]?.turnId), String(turn.turnId));
assert.equal(String(runtimeEvents[compactedIndex]?.turnId), String(turn.turnId));
assert.isAbove(completedIndex, compactedIndex);
}).pipe(
Effect.provideService(Random.Random, makeDeterministicRandomService()),
Effect.provide(harness.layer),
);
});

it.effect("steers a running turn instead of opening a new one on mid-turn sendTurn", () => {
const harness = makeHarness();
return Effect.gen(function* () {
Expand Down
Loading
Loading