Skip to content
Open
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
18 changes: 13 additions & 5 deletions apps/server/src/claudeHistoryWorker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { forkSession, getSessionMessages } from "@anthropic-ai/claude-agent-sdk";
import {
forkSession,
getSessionMessages,
getSubagentMessages,
} from "@anthropic-ai/claude-agent-sdk";
import * as Schema from "effect/Schema";

// A separate process gives SDK history helpers the provider's environment without
Expand All @@ -11,8 +15,10 @@ import * as Schema from "effect/Schema";
const decodeHistoryOptions = Schema.decodeSync(
Schema.fromJsonString(
Schema.Struct({
agentId: Schema.optionalKey(Schema.String),
dir: Schema.optionalKey(Schema.String),
includeSystemMessages: Schema.optionalKey(Schema.Boolean),
limit: Schema.optionalKey(Schema.Number),
upToMessageId: Schema.optionalKey(Schema.String),
}),
),
Expand All @@ -23,15 +29,17 @@ export async function runClaudeHistoryWorker(
sessionId: string | undefined,
rawOptions: string | undefined,
): Promise<void> {
const options = decodeHistoryOptions(rawOptions ?? "{}");
const { agentId, ...options } = decodeHistoryOptions(rawOptions ?? "{}");
if (!sessionId) throw new Error("Claude history session id is required.");
const result =
method === "getSessionMessages"
? await getSessionMessages(sessionId, options)
: method === "forkSession"
? await forkSession(sessionId, options)
: (() => {
throw new Error("Unknown Claude history operation.");
})();
: method === "getSubagentMessages" && agentId
? await getSubagentMessages(sessionId, agentId, options)
: (() => {
throw new Error("Unknown Claude history operation.");
})();
process.stdout.write(JSON.stringify(result));
}
164 changes: 161 additions & 3 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import * as Stream from "effect/Stream";
import * as TestClock from "effect/testing/TestClock";
import { Tool } from "effect/ai";
import { formatClaudeResumeCompactionQuestion } from "@t3tools/shared/claudeCompaction";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess";
import { SpawnExecutableResolution } from "@t3tools/shared/shell";

import { attachmentRelativePath } from "../../attachmentStore.ts";
Expand All @@ -62,7 +62,7 @@ import { WorktreeToolkit } from "../../mcp/toolkits/worktree/tools.ts";
import { ThreadToolkit } from "../../mcp/toolkits/thread/tools.ts";
import { OrchestratorToolkit } from "../../mcp/toolkits/orchestrator/tools.ts";
import { ClaudeExecutableFileCheck } from "../../provider/Drivers/ClaudeExecutable.ts";
import type * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
import type * as ProviderContinuationRequests from "@t3tools/provider-core/server/ProviderContinuationRequests";
import { makeProviderFailure } from "@t3tools/provider-core/server/failure";
Expand Down Expand Up @@ -1826,6 +1826,90 @@ describe("ClaudeAdapterV2 native fork", () => {
);
});

describe("ClaudeAdapterV2 subagent launch lookup", () => {
it.live("reads a subagent stored under the instance's own Claude home", () =>
Effect.scoped(
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const crypto = yield* Crypto.Crypto;
const claudeHome = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-claude-v2-subagent-home-",
});
const workspace = yield* fileSystem.realPath(
yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-claude-v2-subagent-workspace-" }),
);
const sessionId = yield* crypto.randomUUIDv4;
const agentId = "a0123456789abcdef";
const projectDir = path.join(
claudeHome,
"projects",
workspace.replace(/[^a-zA-Z0-9]/g, "-"),
);
const subagentsDir = path.join(projectDir, sessionId, "subagents");
const entry = {
type: "user",
parentUuid: null,
sessionId,
cwd: workspace,
timestamp: "2026-01-01T00:00:00.000Z",
};
yield* fileSystem.makeDirectory(subagentsDir, { recursive: true });
yield* fileSystem.writeFileString(
path.join(projectDir, `${sessionId}.jsonl`),
JSON.stringify({
...entry,
uuid: yield* crypto.randomUUIDv4,
isSidechain: false,
message: { role: "user", content: "start a subagent" },
}),
);
yield* fileSystem.writeFileString(
path.join(subagentsDir, `agent-${agentId}.jsonl`),
JSON.stringify({
...entry,
uuid: yield* crypto.randomUUIDv4,
isSidechain: true,
agentId,
message: { role: "user", content: "subagent prompt" },
}),
);
// The CLI records the launching tool call in the transcript's sidecar.
yield* fileSystem.writeFileString(
path.join(subagentsDir, `agent-${agentId}.meta.json`),
JSON.stringify({ agentType: "general-purpose", toolUseId: "toolu_launch" }),
);
const queryRunner = yield* ClaudeAdapterV2.ClaudeAgentSdkQueryRunner;

const toolUseId = yield* queryRunner.subagentLaunchToolUseId({
sessionId,
agentId,
dir: workspace,
threadId: ThreadId.make("thread-claude-subagent-instance-home"),
providerSessionId: ProviderSessionId.make(
"provider-session-claude-subagent-instance-home",
),
environment: { ...(yield* HostProcessEnvironment), CLAUDE_CONFIG_DIR: claudeHome },
});

assert.equal(toolUseId, "toolu_launch");
}).pipe(
Effect.provide(
ClaudeAdapterV2.layerQueryRunner.pipe(
Layer.provideMerge(
Layer.succeed(
ProviderEventLoggers.ProviderEventLoggers,
ProviderEventLoggers.NoOpProviderEventLoggers,
),
),
Layer.provideMerge(NodeServices.layer),
),
),
),
),
);
});

describe("ClaudeAdapterV2 native session identity", () => {
const openTurnWithOrdinal = (providerTurnOrdinal: number, nativeThreadHasTurns?: boolean) =>
Effect.scoped(
Expand Down Expand Up @@ -2107,6 +2191,7 @@ describe("ClaudeAdapterV2 background wake turns", () => {
readonly close?: (sdkMessages: Queue.Queue<SDKMessage>) => Effect.Effect<void>;
readonly interrupt?: Effect.Effect<void>;
readonly environment?: NodeJS.ProcessEnv;
readonly subagentLaunchToolUseId?: ClaudeAdapterV2.ClaudeAgentSdkQueryRunnerShape["subagentLaunchToolUseId"];
// A CLI process opened after the first streams from its own queue, so the
// first one can exit (Queue.shutdown) and a later turn can start another.
readonly freshQueueOnReopen?: boolean;
Expand Down Expand Up @@ -2207,7 +2292,7 @@ describe("ClaudeAdapterV2 background wake turns", () => {
};
}),
forkSession: () => Effect.die("unused forkSession"),
subagentLaunchToolUseId: () => Effect.succeed(null),
subagentLaunchToolUseId: options?.subagentLaunchToolUseId ?? (() => Effect.succeed(null)),
assertComplete: Effect.void,
},
});
Expand Down Expand Up @@ -2275,6 +2360,79 @@ describe("ClaudeAdapterV2 background wake turns", () => {
});
const makeWakeHarness = makeWakeHarnessWithOptions();

it.effect("looks up a resumed subagent's launch in the instance's environment", () =>
Effect.scoped(
Effect.gen(function* () {
const SEND = "toolu-instance-home-sendmessage";
const lookups: Array<ClaudeAdapterV2.ClaudeAgentSdkSubagentLookupInput> = [];
const harness = yield* makeWakeHarnessWithOptions({
environment: { CLAUDE_CONFIG_DIR: "/instance/claude-home" },
subagentLaunchToolUseId: (input) =>
Effect.sync(() => {
lookups.push(input);
return null;
}),
});
yield* harness.runtime.startTurn(
makeClaudeTestTurnInput({
threadId: harness.threadId,
providerThread: harness.providerThread,
now: yield* DateTime.now,
attemptId: RunAttemptId.make("attempt-claude-instance-home-resume"),
text: "Ask the auditor to check again.",
attachments: [],
}),
);
// A subagent launched before a restart: its SendMessage resume names
// a task this session never saw start.
yield* harness.offerAndWait(
claudeSdkFrame({
type: "assistant",
message: {
model: "claude-sonnet-4-6",
id: "msg_instance_home_sendmessage",
type: "message",
role: "assistant",
content: [
{
type: "tool_use",
id: SEND,
name: "SendMessage",
input: { to: "task-before-restart", message: "Check again." },
},
],
},
parent_tool_use_id: null,
uuid: "00000000-0000-4000-8000-000000002101",
session_id: WAKE_NATIVE_SESSION,
}),
);
yield* harness.offerAndWait(
claudeSdkFrame({
type: "system",
subtype: "task_started",
task_id: "task-before-restart",
tool_use_id: SEND,
description: "Audit recent commits",
is_backgrounded: true,
task_type: "local_agent",
prompt: "Check again.",
uuid: "00000000-0000-4000-8000-000000002102",
session_id: WAKE_NATIVE_SESSION,
}),
);
assert.deepEqual(
lookups.map((lookup) => lookup.environment?.CLAUDE_CONFIG_DIR),
["/instance/claude-home"],
);
}).pipe(
Effect.provide(
Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
),
),
),
);

it.effect.each([
{ isError: false, title: "Check weather" },
{ isError: true, title: "Check weather" },
Expand Down
Loading
Loading