Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f371b19
feat(server): fx gets update checks and the Update now button
jakeleventhal Oct 29, 2026
6cd1ef9
fix(server): fx 0.0.15 tools and sub-agents cut off by Stop show as i…
jakeleventhal Oct 29, 2026
4b5dfa8
fix(server): a tool Stop cut off stays interrupted through later updates
jakeleventhal Oct 29, 2026
009dd0d
fix(server): no fx update offer on macOS 14
jakeleventhal Oct 29, 2026
fc336db
test(server): the Stop test checks the follow-up output reached the tool
jakeleventhal Oct 29, 2026
eb8b52d
fix(fx): preserve real tool failures during Stop
jakeleventhal Nov 2, 2026
7ad71f3
fix(fx): update Homebrew installs through their owning brew
jakeleventhal Nov 2, 2026
744d008
fix(fx): leave mise-managed updates to mise
jakeleventhal Nov 2, 2026
97acf7d
fix(fx): detect configured mise roots and retain advisories
jakeleventhal Nov 2, 2026
efd23b1
refactor(fx): read kernel release through the host context
jakeleventhal Nov 2, 2026
30e993f
fix(fx): use the first tool result when detecting cancellation
jakeleventhal Nov 2, 2026
336badc
fix(fx): retain errors across mixed tool content
jakeleventhal Nov 2, 2026
579de19
fix(fx): recognize cancellation errors from fx 0.0.16
maria-rcks Nov 2, 2026
9de3b1b
fix(fx): merge main while preserving cancellation handling
maria-rcks Nov 2, 2026
4585b90
fix(fx): preserve subagent failures after stop
maria-rcks Nov 2, 2026
b903a24
test(fx): keep cancellation fixture options typed
maria-rcks Nov 2, 2026
1dae9ac
fix(fx): merge main while preserving update and cancellation support
maria-rcks Nov 3, 2026
9d8eb86
fix(fx): classify first cancelled tool updates as interrupted
maria-rcks Nov 3, 2026
ffbbe21
fix(fx): require mise ownership in shared shim directories
maria-rcks Nov 3, 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
39 changes: 39 additions & 0 deletions apps/server/scripts/acp-mock-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1290,8 +1290,27 @@ const program = Effect.gen(function* () {
status: "in_progress",
},
});
yield* agent.client.sessionUpdate({
sessionId: requestedSessionId,
update: {
sessionUpdate: "tool_call_update",
toolCallId: "native-failed-tool",
title: "Failing command",
kind: "execute",
status: "in_progress",
},
});
yield* Deferred.await(nativeCancelRequested);
yield* Deferred.await(nativeCancelRelease);
yield* agent.client.sessionUpdate({
sessionId: requestedSessionId,
update: {
sessionUpdate: "tool_call_update",
toolCallId: "native-failed-tool",
status: "failed",
content: [{ type: "content", content: { type: "text", text: "Permission denied" } }],
},
});
yield* agent.client.sessionUpdate({
sessionId: requestedSessionId,
update: {
Expand All @@ -1301,6 +1320,26 @@ const program = Effect.gen(function* () {
content: [{ type: "content", content: { type: "text", text: "Cancelled." } }],
},
});
yield* agent.client.sessionUpdate({
sessionId: requestedSessionId,
update: {
sessionUpdate: "tool_call_update",
toolCallId: "native-unseen-cancel-tool",
title: "Cancelled command",
kind: "execute",
status: "failed",
content: [{ type: "content", content: { type: "text", text: "Cancelled." } }],
},
});
// A later output-only update must not undo the interrupted status.
yield* agent.client.sessionUpdate({
sessionId: requestedSessionId,
update: {
sessionUpdate: "tool_call_update",
toolCallId: "native-cancel-tool",
content: [{ type: "content", content: { type: "text", text: "Cancelled. exit 130" } }],
},
});
yield* agent.client.sessionUpdate({
sessionId: requestedSessionId,
update: {
Expand Down
6 changes: 3 additions & 3 deletions apps/server/scripts/record-fx-acp-replay-fixture.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/**
* Records an fx ACP replay transcript through the real orchestrator and
* FxAdapterV2, teeing only the protocol logger. Run from apps/server with
* fx 0.0.13 on PATH (or T3_FX_BIN); HOME stays the user's real login:
* Records an fx ACP replay transcript through the real orchestrator and FxAdapterV2.
* Run from apps/server with fx on PATH (or T3_FX_BIN), signed in under HOME:
* node scripts/record-fx-acp-replay-fixture.ts --scenario simple
* fx 0.0.14+ saves each recorded mode as the default in ~/.fx/settings.json; restore it after.
*/
import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
import * as NodeServices from "@effect/platform-node/NodeServices";
Expand Down
70 changes: 66 additions & 4 deletions apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5424,6 +5424,26 @@ describe("AcpAdapterV2", () => {
flavor: {
driver: ACP_TEST_DRIVER,
capabilities: AcpProviderCapabilitiesV2,
extractSubagentUpdate: (tool) =>
tool.toolCallId === "native-failed-tool"
? {
nativeTaskId: tool.toolCallId,
childSessionId: null,
prompt: "Run a command",
title: null,
model: null,
status: tool.status === "failed" ? "failed" : "running",
result: tool.status === "failed" ? "Permission denied" : null,
suppressNormalTool: tool.status !== "failed",
}
: undefined,
isCancelledToolCall: Schema.is(
Schema.Struct({
update: Schema.Struct({
toolCallId: Schema.Literals(["native-cancel-tool", "native-unseen-cancel-tool"]),
}),
}),
),
makeRuntime: makeMockRuntime({
childProcessSpawner,
mockAgentPath: yield* path.fromFileUrl(
Expand Down Expand Up @@ -5494,15 +5514,57 @@ describe("AcpAdapterV2", () => {
);
yield* native.current.request("_test/finish-cancel", {});
yield* Fiber.join(interrupt);
const terminal = Option.getOrThrow(
const untilTerminal = Array.from(
yield* runtime.events.pipe(
Stream.filter(
Stream.takeUntil(
(event) => event.type === "turn.terminal" && event.providerTurnId === providerTurnId,
),
Stream.runHead,
Stream.runCollect,
),
);
assert.equal(terminal.type === "turn.terminal" && terminal.status, "interrupted");
const terminal = untilTerminal.at(-1);
assert.equal(terminal?.type === "turn.terminal" && terminal.status, "interrupted");
// The agent fails the command Stop cut off, then updates its output.
const command = untilTerminal
.flatMap((event) =>
event.type === "turn_item.updated" &&
event.turnItem.nativeItemRef?.nativeId === "native-cancel-tool"
? [event.turnItem]
: [],
)
.at(-1);
const unseenCommand = untilTerminal
.flatMap((event) =>
event.type === "turn_item.updated" &&
event.turnItem.nativeItemRef?.nativeId === "native-unseen-cancel-tool"
? [event.turnItem]
: [],
)
.at(-1);
assert.equal(unseenCommand?.status, "interrupted");
assert.equal(command?.status, "interrupted");
assert.include(command?.type === "command_execution" ? command.output : "", "exit 130");
const failure = untilTerminal
.flatMap((event) =>
event.type === "turn_item.updated" &&
event.turnItem.nativeItemRef?.nativeId === "native-failed-tool"
? [event.turnItem]
: [],
)
.at(-1);
assert.equal(failure?.status, "failed");
const subagent = untilTerminal
.flatMap((event) =>
event.type === "turn_item.updated" && event.turnItem.type === "subagent"
? [event.turnItem]
: [],
)
.at(-1);
assert.equal(subagent?.status, "failed");
assert.include(
failure?.type === "command_execution" ? failure.output : "",
"Permission denied",
);
yield* runtime.startTurn(
makeTurnInput({ threadId, providerThread, instanceId, runtimePolicy, now, ordinal: 2 }),
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { assert } from "@effect/vitest";
import type { ProviderReplayTranscript } from "@t3tools/contracts";

import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
import {
assertBaseProjection,
assertSemanticProjectionIntegrity,
assertUserMessagesInclude,
FX_SUBAGENT_STOP_PROMPT,
projectionFor,
} from "../shared.ts";

/** fx 0.0.15 fails a sub-agent that Stop cut off; T3 shows it, and its child thread, as interrupted. */
export function assertFxSubagentStopOutput(
result: OrchestratorV2ScenarioResult,
transcript: ProviderReplayTranscript,
) {
assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["interrupted"] });

const projection = projectionFor(result, transcript.scenario);
assertSemanticProjectionIntegrity(projection);
assertUserMessagesInclude(projection, [FX_SUBAGENT_STOP_PROMPT]);
assert.notInclude(
projection.turnItems.map((item) => item.type),
"dynamic_tool",
"the subagent call must not also show as a tool row",
);
assert.lengthOf(projection.subagents, 1);
const subagent = projection.subagents[0]!;
assert.equal(subagent.status, "interrupted");
assert.include(subagent.prompt, "stopped.txt");
assert.equal(subagent.result, "Cancelled", "fx's error, not its JSON envelope");
if (subagent.childThreadId === null) throw new Error("The subagent has no child thread");
assert.equal(
result.projections.get(subagent.childThreadId)?.thread.lineage.parentThreadId,
projection.thread.id,
);
assert.equal(projection.providerTurns[0]?.status, "interrupted");
assert.equal(projection.providerThreads[0]?.status, "idle");
}
Loading
Loading