mirror of
https://github.com/vercel-labs/fx.git
synced 2026-10-28 10:15:17 +08:00
Update tests and the design record for the new handle, label, and retry
Tests that read command replays matched the old handle, fx-command-replay- and the hash, so they found none once each run's handle gained a run id. They now accept the run id and take the blob's name from the hash after it. D91 in the session manager's design record amends D44 with the new handle shape. A name the registry lacks but that has the mcp_ prefix now counts as an MCP call, so the unsupported-tool test expects "1 command · 1 MCP call". The ACP length test scripts the retry's reply and checks that the retry carries a 32,768-token limit and never asks for a summary.
This commit is contained in:
8 files changed
+37
-11
No files matched your search
@@ -267,7 +267,8 @@ const ReplayBacking = union(enum) {
|
||||
/// blob, and `.bin` (D44). Caller owns.
|
||||
/// One run's handle for the blob that holds its output. The blob's hash comes
|
||||
/// last, where `artifact_digest.blobHash` reads it, after a random id for the
|
||||
/// run, so two commands with the same output still get handles of their own.
|
||||
/// run, so two commands with the same output still get handles of their own
|
||||
/// (D91).
|
||||
fn blobReplayHandle(alloc: Allocator, hash: session_child_store.Blobs.Hash) ![]u8 {
|
||||
var run_id: [6]u8 = undefined;
|
||||
try std.Io.randomSecure(io_mod.getIo(), &run_id);
|
||||
|
||||
@@ -8833,6 +8833,7 @@ describe("acp: model-independent", () => {
|
||||
fakeGatewayToolCall("read_1", "read_file", { path: "a.txt" }),
|
||||
fakeGatewayToolCall("read_2", "read_file", { path: "b.txt" }),
|
||||
noToolLength(),
|
||||
noToolLength(),
|
||||
finalText("UNEXPECTED_CONTINUATION"),
|
||||
]);
|
||||
try {
|
||||
@@ -8848,7 +8849,11 @@ describe("acp: model-independent", () => {
|
||||
);
|
||||
|
||||
expect(result.promptResult.result.stopReason).toBe("max_output_tokens");
|
||||
expect(gateway.requests).toHaveLength(3);
|
||||
// The empty reply's one retry sends the same request with an output
|
||||
// limit; neither request asks for a summary.
|
||||
expect(gateway.requests).toHaveLength(4);
|
||||
expect(JSON.parse(gateway.requests[2]!.body).maxOutputTokens).toBeUndefined();
|
||||
expect(JSON.parse(gateway.requests[3]!.body).maxOutputTokens).toBe(32_768);
|
||||
expect(gateway.requests.some(({ body }) =>
|
||||
body.includes("Summarize what you just did.")
|
||||
)).toBe(false);
|
||||
|
||||
@@ -5929,7 +5929,7 @@ printf '%s' ${JSON.stringify(trailingMarker)} > ${JSON.stringify(effectPath)}
|
||||
expect(latest.code).toBe(0);
|
||||
const sessionId = JSON.parse(latest.stdout).id as string;
|
||||
// One command replay, kept as a blob of the session.
|
||||
const replays = new Set(savedText(root.home, sessionId).match(/fx-command-replay-[a-f0-9]{64}\.bin/g) ?? []);
|
||||
const replays = new Set(savedText(root.home, sessionId).match(/fx-command-replay-[a-f0-9]{12}-[a-f0-9]{64}\.bin/g) ?? []);
|
||||
expect(replays.size).toBe(1);
|
||||
const replayHash = /([a-f0-9]{64})\.bin$/.exec([...replays][0]!)![1]!;
|
||||
expect(existsSync(join(sessionDir(root.home, sessionId), "blobs", replayHash))).toBe(true);
|
||||
|
||||
@@ -112,10 +112,18 @@ export function compactionRecordPath(home: string, id: string, name: string): st
|
||||
}
|
||||
|
||||
/// The command replays session `id` kept: each is one of its blobs, named in
|
||||
/// the session by its handle, `fx-command-replay-<hash>.bin` (D44).
|
||||
/// the session by its handle, `fx-command-replay-<run>-<hash>.bin` (D44, D91).
|
||||
/// A handle from before D91 has no run id.
|
||||
export function commandReplays(home: string, id: string): { name: string; path: string }[] {
|
||||
const names = [...new Set(savedText(home, id).match(/fx-command-replay-[0-9a-f]{64}\.bin/g) ?? [])];
|
||||
return names.map((name) => ({ name, path: join(sessionDir(home, id), "blobs", name.slice("fx-command-replay-".length, -".bin".length)) }));
|
||||
const names = [...new Set(savedText(home, id).match(commandReplayHandles) ?? [])];
|
||||
return names.map((name) => ({ name, path: join(sessionDir(home, id), "blobs", commandReplayHash(name)) }));
|
||||
}
|
||||
|
||||
const commandReplayHandles = /fx-command-replay-(?:[0-9a-f]{12}-)?[0-9a-f]{64}\.bin/g;
|
||||
|
||||
/// The hash of the blob a command replay handle names, which comes last.
|
||||
export function commandReplayHash(handle: string): string {
|
||||
return /([0-9a-f]{64})\.bin$/.exec(handle)![1]!;
|
||||
}
|
||||
|
||||
/// Every tool call a saved history holds, finished or left open by an
|
||||
|
||||
@@ -525,7 +525,7 @@ test("a long command output is kept as one read-only blob, and the model pages i
|
||||
const gateway = startDynamicFakeGateway(async (body) => {
|
||||
if (body.includes("Page the output again.")) {
|
||||
if (body.includes("replay-read-1")) return fakeGatewayFinalText("REPLAY_PAGED_DONE");
|
||||
const handle = /fx-command-replay-[a-f0-9]{64}\.bin/.exec(body)?.[0] ?? "missing-handle";
|
||||
const handle = /fx-command-replay-[a-f0-9]{12}-[a-f0-9]{64}\.bin/.exec(body)?.[0] ?? "missing-handle";
|
||||
return fakeGatewayToolCall("replay-read-1", "read_tool_result", {
|
||||
request: { handle, query: "150001" },
|
||||
});
|
||||
@@ -539,8 +539,9 @@ test("a long command output is kept as one read-only blob, and the model pages i
|
||||
expect(JSON.parse(created.stdout).output).toBe("LONG_RUN_DONE");
|
||||
const id = JSON.parse(created.stdout).session_id;
|
||||
|
||||
const handle = /fx-command-replay-[a-f0-9]{64}\.bin/.exec(JSON.stringify(logLines(fixture.home, id)))?.[0] ?? "";
|
||||
expect(handle).toMatch(/^fx-command-replay-[a-f0-9]{64}\.bin$/);
|
||||
const handle = /fx-command-replay-[a-f0-9]{12}-[a-f0-9]{64}\.bin/.exec(JSON.stringify(logLines(fixture.home, id)))?.[0] ?? "";
|
||||
// A run id, then the hash that names the blob (D44, D91).
|
||||
expect(handle).toMatch(/^fx-command-replay-[a-f0-9]{12}-[a-f0-9]{64}\.bin$/);
|
||||
const hash = /-([a-f0-9]{64})\./.exec(handle)![1]!;
|
||||
expect(blobNames(fixture, id)).toContain(hash);
|
||||
expect(blobNames(fixture, id).every((name) => /^[a-f0-9]{64}$/.test(name))).toBe(true);
|
||||
|
||||
@@ -5994,7 +5994,8 @@ describe.skipIf(!tmuxAvailable())("TUI gateway stream lifecycle", () => {
|
||||
await session.waitForComposer(TIMEOUT);
|
||||
await session.sendText("Run the supported call after the unknown call.");
|
||||
await session.waitForText(finalText, TIMEOUT);
|
||||
const header = "● 2 tool calls · 2 commands · 1 failed";
|
||||
// The unsupported name has the MCP prefix, so it counts as an MCP call.
|
||||
const header = "● 2 tool calls · 1 command · 1 MCP call · 1 failed";
|
||||
const failedRow = `├ Failed ${unsupportedToolName}`;
|
||||
const completedRow = `└ Ran ${supportedCommand}`;
|
||||
const compact = await session.captureFullScrollback();
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ TLA+ specifications for fx, checked with TLC, together with the scripts and Bun
|
||||
| `mcp_host/` | fx's MCP host (`src/core/mcp_host/`): host lifecycle, saved credentials, logins shared between processes, logins beside processes that run an older build (`MixedBuilds.tla`), sign-in, elicitation routing, and calls held for the URLs a server asks the user to open. | `mcp_host/check.sh`, `bun test drivers/mcp_host/` |
|
||||
| `sdk/` | libfx durable sessions (`DurableSession.tla`), how a session object takes prompts in (`PromptIntake.tla`), images saved once by name (`ImageStore.tla`), agent names and routing (`AgentRouting.tla`), tenants whose agents share one process (`TenantIsolation.tla`), steering (`DurableSteer.tla`), and the provider credential edge (`ProviderAuth.tla`). `check.sh` also writes the TLC state graphs the drivers walk. | `sdk/check.sh`, then the drivers |
|
||||
| `session/` | The session the interactive app has open across launches, prompts, `/new`, `/clear`, `/resume`, `/fork`, quitting and crashes, and what `/status` and the exit line say about it (`src/core/session/`, D90). | `session/check.sh`, then `bun test ./tests/tla/drivers/session/`, which runs the real binary along it and has TLC check every run |
|
||||
| `session_manager/` | The session log, turn lifecycle, writer lock, snapshots, fork, subagents, catalog, host wiring, the side-file move, v1 conversion, and prompt images saved as blobs (`PromptImages.tla`). `rollout/` holds the models of the changes that moved fx onto the session manager. `system-design.md` is the design record the specs check and defines the `D1` to `D90` decision numbers that code comments cite. | `session_manager/check.sh`, `session_manager/mutants.py`, `session_manager/rollout/run.py`, `bun test drivers/session_manager/` |
|
||||
| `session_manager/` | The session log, turn lifecycle, writer lock, snapshots, fork, subagents, catalog, host wiring, the side-file move, v1 conversion, and prompt images saved as blobs (`PromptImages.tla`). `rollout/` holds the models of the changes that moved fx onto the session manager. `system-design.md` is the design record the specs check and defines the `D1` to `D91` decision numbers that code comments cite. | `session_manager/check.sh`, `session_manager/mutants.py`, `session_manager/rollout/run.py`, `bun test drivers/session_manager/` |
|
||||
| `shell_command/` | How an agent git command is admitted and run (`src/core/shell_command/safe_git.zig`), and one model turn through the shell tool, for the current design and a redesign. | `shell_command/check.sh`, `bun test drivers/shell_command/` |
|
||||
| `terminal/` | The in-memory shell snapshot owner (`src/core/terminal/shell_snapshot.zig`) and the lifecycle of hosted terminals. | `terminal/check.sh` |
|
||||
| `usage/` | `/usage` refreshes and the shared usage cache (`src/usage/`): `LeanUsage.tla` and `UsageModel.tla`. | `usage/check.sh`, then the drivers in `drivers/usage/` |
|
||||
|
||||
@@ -1561,6 +1561,16 @@ only by quitting):
|
||||
can be resumed yet. **Alternative:** show the id only once saved; print
|
||||
the full `fx --resume` command; or keep counting turns and show a fork
|
||||
before its first prompt as unsaved, though resume opens it.
|
||||
- **D91 Decision (amends D44):** a command's output handle is
|
||||
`fx-command-replay-`, a random 12-hex id for the run, `-`, the body's
|
||||
full hash and `.bin`. Readers take the hash after the handle's last `-`,
|
||||
as they do for every v2 handle, so no name index exists, a handle from
|
||||
before this decision still resolves, and two runs with the same output
|
||||
keep sharing one blob. **Why:** a handle that was only the output's hash
|
||||
gave two commands with identical output the same handle, so the model
|
||||
read a later result as a replay of an earlier one. **Alternative:** hash
|
||||
the command into the body, which changes the replay format and still
|
||||
repeats a handle when the same command prints the same output twice.
|
||||
|
||||
## Design Risks And Failure Behavior
|
||||
|
||||
|
||||
Reference in new issue
Block a user