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
2 changes: 1 addition & 1 deletion .agents/skills/verify-mcode/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Composer-queue `check` runs the deterministic verifier checks. Its tests do not

Use `runtime live --provider codex --model gpt-5.6-terra --scenario subagent --confirm-provider-call` for the Codex V2 subagent persistence journey. Read `references/features/codex-subagent-view.md` and complete its Electron steps for navigation, color, and reload proof.

Run `runtime worktree-setup --confirm-cleanup` after changes to managed-worktree creation or automatic Setup. It creates an owned Git project, starts a queued New-worktree turn, proves automatic Setup reads the completed checkout, and removes all generated state without making a provider call. If a proof is interrupted, run `runtime worktree-setup-cleanup --confirm-cleanup` before retrying.
Run `runtime worktree-setup --confirm-cleanup` after changes to managed-worktree creation or automatic Setup. It creates an owned Git project, drops the first `agent.createAndSend` response, then retries the same request after a second client observes its bound startup. It proves one persisted thread, worktree, and queued first turn, checks that Setup reads the completed checkout, and removes all generated state without making a provider call. If a proof is interrupted, run `runtime worktree-setup-cleanup --confirm-cleanup` before retrying.

Run `runtime console-audit` after changes to child-process spawn, startup, or cleanup code on Windows. It lists visible windows owned by runtime-process descendants; the server tree must never own one. Use `--watch <seconds>` across a runtime restart or packaged launch to catch transient flashes, and read `references/features/windows-console-hygiene.md` for proof and Terminal-hosting limits.

Expand Down
3 changes: 1 addition & 2 deletions .agents/skills/verify-mcode/references/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
| Codex subagents retain task, state, transcript, navigation, and identity color across both protocol shapes | [Codex subagent view](codex-subagent-view.md) | `runtime check`, Terra `runtime live --scenario subagent`, and Electron UI proof |
| Thread deletion and provider-session cleanup retain runtime ownership | [Resource lifecycle](resource-lifecycle.md) | `runtime check` and controlled thread cleanup |
| Pointer-selected assistant text opens a compact comment editor and retains native copy actions | [Selected text comments](selected-text-comments.md) | Electron public UI proof |
| A New worktree completes checkout before automatic Setup starts and can cancel a held Setup safely | [Managed-worktree Setup readiness](managed-worktree-setup.md) | `runtime worktree-setup --confirm-cleanup` and `runtime check` |
| A lost New-worktree create response can be retried with one startup ID; checkout completes before automatic Setup and held Setup can be cancelled safely | [Managed-worktree Setup readiness](managed-worktree-setup.md) | `runtime worktree-setup --confirm-cleanup` and `runtime check` |
| Local, managed-worktree, and PR-created threads show truthful startup progress and remove it after success | [Thread startup progress](thread-startup-progress.md) | Electron public UI proof and focused startup tests |
| A user completes a worktree thread and the app schedules its cleanup | [Completed-thread cleanup](completed-thread-cleanup.md) | `thread-lifecycle proof --confirm-cleanup` and `thread-lifecycle check` |
| Queued composer messages continue in FIFO order after completion and stay paused after Stop | [Composer queue](composer-queue.md) | `composer-queue proof --cursor-model <id> --allow-enable-cursor --confirm-provider-calls --confirm-cleanup` |
Expand Down Expand Up @@ -77,7 +77,6 @@ Read [Multi-surface journeys](multi-surface-journeys.md) for a workflow that cro
- The selected-text-comments live proof covers rendered source cards. It does not load a source that is absent from the current transcript window.
- Saved-comment edit, delete, and focus return need the card and marker entry points planned for #1557 and #1558.
- Thread retention has a minimum of one day. The live proof shows the scheduled deletion, while focused integration checks show later worktree cleanup.
- If `agent.createAndSend` creates a thread but its response is lost before the ID arrives, the public RPC has no safe cleanup identifier. Record this as a coverage gap. Do not delete threads by heuristic.
- If `thread.create` creates a managed-worktree thread but its response is lost before the ID arrives, the lifecycle verifier has no safe cleanup identifier. Record this as a coverage gap. Do not delete threads by heuristic.
- The ACP narrative fixture does not emit permission prompts, plans, subagents, or usage updates; those paths still need live provider coverage or focused tests.
- The Windows console audit cannot attribute Windows Terminal-hosted console windows to a process tree; watch-mode sightings need manual correlation, and an uncorrelated sighting is a gap rather than a pass or defect.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Behavior

- Mcode returns a managed worktree only after Git completes its checkout.
- A second client can retry the same create request after losing the first response. The startup ID still identifies one bound startup, one thread, one worktree, and one persisted queued first turn.
- The first turn enters the automatic Setup gate after the managed worktree exists.
- Automatic Setup runs in the managed worktree, not the source workspace.
- Setup reads every tracked fixture file before it writes the proof marker.
Expand All @@ -15,9 +16,9 @@
1. Start the runtime and run `runtime health`.
2. Run `runtime check` for the focused Git failure regression.
3. Run `runtime worktree-setup --confirm-cleanup`.
4. Inspect the redacted receipt. It must report the complete checkout, a terminal cancelled startup, a cancelled Setup step, an interrupted Setup attempt, a stopped fixture process, no agent runtime, no provider call, and successful cleanup.
4. Inspect the redacted receipt. `lostResponse` must report one bound startup, thread, worktree, queued turn, and persisted prompt after the retry. It must also report the complete checkout, a terminal cancelled startup, a cancelled Setup step, an interrupted Setup attempt, a stopped fixture process, no agent runtime, no provider call, and successful cleanup.

The verifier holds Setup open after the marker. It records the fixture process ID, then uses `thread.startup.cancel`. The first turn stays queued, so this proof does not call a provider.
The verifier discards the first `agent.createAndSend` response, observes the bound startup through a second authenticated socket, closes the first socket, and retries the exact request. It holds Setup open after the marker, records the fixture process ID, then uses `thread.startup.cancel`. The first turn stays queued, so this proof does not call a provider.

## Failure handling

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,8 @@ Use `runtime check` for both protocol shapes. Use `runtime live --scenario subag

## Managed-worktree Setup readiness

1. Create a New-worktree first turn through the public agent API.
2. Wait for Git to finish the checkout before the thread is returned.
1. Send a New-worktree first turn through the public agent API with one startup ID and discard its response.
2. From a second client, observe that the startup is bound to a thread, close the first client, and retry the exact request. The response must wait for Git checkout to finish. Confirm one durable startup, thread, worktree, queued first turn, and user prompt.
3. Keep the first turn queued while automatic Setup reads every tracked fixture file, records its PID, and writes its proof marker.
4. Cancel startup through the public API. Confirm the terminal startup state, stopped Setup process, interrupted Setup attempt, queued first turn, and no agent runtime.
5. Remove the generated thread, worktree, workspace, and fixture repository.
Expand Down
101 changes: 95 additions & 6 deletions .agents/skills/verify-mcode/scripts/runtime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ Commands:
with the project-scoped always-allow option, records the .devin config file it writes, and
removes that grant after the proof.
worktree-setup --confirm-cleanup
Create an owned Git project, verify its held automatic Setup gate, cancel Setup through the public API, then remove the project, workspace, thread, and worktree. Does not make a provider call.
Create an owned Git project, lose the first create response, retry with the same startup ID, verify held Setup, cancel it, then remove owned state. Does not make a provider call.
worktree-setup-cleanup --confirm-cleanup
Remove a retained worktree-setup run after an interrupted proof.
diagnostics
Expand Down Expand Up @@ -544,14 +544,25 @@ async function worktreeSetup(repoRoot) {
const report = createWorktreeSetupReport();
const deadline = Date.now() + LIVE_TIMEOUT_MS;
let socket = null;
let firstSocket = null;
try {
await health(repoRoot);
socket = await openSocket(repoRoot, readRuntime(repoRoot));
firstSocket = await openSocket(repoRoot, readRuntime(repoRoot));
socket = firstSocket;
await createWorktreeSetupFixture(run.record);
const workspace = await createWorktreeSetupWorkspace(socket, run.evidenceDirectory, run.record, deadline);
await saveWorktreeSetupConfiguration(socket, workspace.id, deadline);
const thread = await createWorktreeSetupThread(socket, workspace.id, run.record.startupId, deadline);
const request = worktreeSetupCreateRequest(workspace.id, run.record.startupId);
await firstSocket.sendWithoutResponse("agent.createAndSend", request);
socket = await openSocket(repoRoot, readRuntime(repoRoot));
const boundThreadId = await waitForBoundWorktreeSetup(socket, run.record, deadline);
await firstSocket.close();
const thread = await createWorktreeSetupThread(socket, request, deadline);
if (thread.id !== boundThreadId) {
throw actionable("The retried create request returned a different thread", "Run worktree-setup-cleanup with --confirm-cleanup, inspect runtime diagnostics, then retry.");
}
recordWorktreeSetupThread(run.evidenceDirectory, run.record, thread);
report.lostResponse = await proveSingleWorktreeSetupCreation(socket, run.record, request.content, deadline);
report.checkout = await proveWorktreeSetup(socket, run.evidenceDirectory, run.record, deadline);
report.cancellation = await cancelWorktreeSetup(socket, run.record, deadline);
} catch (error) {
Expand All @@ -563,13 +574,15 @@ async function worktreeSetup(repoRoot) {
report.cleanup.failure = safeError(error);
}
await socket?.close();
if (firstSocket && firstSocket !== socket) await firstSocket.close();
}
return writeWorktreeSetupReceipt(repoRoot, run.receiptPath, report);
}

function createWorktreeSetupReport() {
return {
command: "worktree-setup",
lostResponse: null,
checkout: null,
cancellation: null,
cleanup: {
Expand Down Expand Up @@ -705,8 +718,8 @@ function worktreeSetupFixtureScript() {
].join("\n");
}

async function createWorktreeSetupThread(socket, workspaceId, startupId, deadline) {
const thread = await socket.rpc("agent.createAndSend", {
function worktreeSetupCreateRequest(workspaceId, startupId) {
return {
workspaceId,
startupId,
content: "Verify automatic Setup checkout readiness.",
Expand All @@ -715,7 +728,24 @@ async function createWorktreeSetupThread(socket, workspaceId, startupId, deadlin
permissionMode: "full",
mode: "worktree",
branch: "main",
}, deadline);
};
}

async function waitForBoundWorktreeSetup(socket, record, deadline) {
while (Date.now() < deadline) {
const startup = await socket.rpc("thread.startup.get", { startupId: record.startupId }, deadline);
if (startup?.threadId) {
if (startup.workspaceId === record.workspaceId && startup.kind === "managed-worktree") return startup.threadId;
break;
}
if (startup && ["failed", "cancelled", "interrupted"].includes(startup.state)) break;
await delayUntil(deadline);
}
throw actionable("The first create request did not bind an owned managed-worktree startup", "Run worktree-setup-cleanup with --confirm-cleanup, inspect runtime diagnostics, then retry.");
}

async function createWorktreeSetupThread(socket, request, deadline) {
const thread = await socket.rpc("agent.createAndSend", request, deadline);
if (
typeof thread?.id !== "string" ||
typeof thread?.worktree_path !== "string" ||
Expand All @@ -728,6 +758,49 @@ async function createWorktreeSetupThread(socket, workspaceId, startupId, deadlin
return thread;
}

async function proveSingleWorktreeSetupCreation(socket, record, prompt, deadline) {
const [threads, startups, automatic, messages] = await Promise.all([
socket.rpc("thread.list", { workspaceId: record.workspaceId }, deadline),
socket.rpc("thread.startup.list", { workspaceId: record.workspaceId }, deadline),
socket.rpc("workspace.environment.automaticSetup.get", { threadId: record.threadId }, deadline),
socket.rpc("message.list", { threadId: record.threadId, limit: 10 }, deadline),
]);
if (!hasOneWorktreeSetupThread(threads, record.threadId)) {
throw actionable("The lost-response retry did not retain exactly one thread", "Run worktree-setup-cleanup with --confirm-cleanup, inspect runtime diagnostics, then retry.");
}
if (!hasOneBoundWorktreeSetup(startups, record)) {
throw actionable("The lost-response retry did not retain exactly one bound startup", "Run worktree-setup-cleanup with --confirm-cleanup, inspect runtime diagnostics, then retry.");
}
if (!hasQueuedFirstTurn(automatic)) {
throw actionable("The lost-response retry did not retain exactly one queued first Turn", "Run worktree-setup-cleanup with --confirm-cleanup, inspect runtime diagnostics, then retry.");
}
if (!hasOnePersistedWorktreeSetupPrompt(messages, automatic.queuedTurns[0].messageId, prompt)) {
throw actionable("The lost-response retry did not retain one durable first-turn prompt", "Run worktree-setup-cleanup with --confirm-cleanup, inspect runtime diagnostics, then retry.");
}
const listed = await runFixtureGit(record.sourceRepositoryPath, ["worktree", "list", "--porcelain"]);
const worktrees = fixtureWorktreePaths(listed).filter((path) => !pathsMatch(path, record.sourceRepositoryPath));
if (worktrees.length !== 1 || !pathsMatch(worktrees[0], record.worktreePath)) {
throw actionable("The lost-response retry did not retain exactly one managed worktree", "Run worktree-setup-cleanup with --confirm-cleanup, inspect runtime diagnostics, then retry.");
}
return { firstResponseUnavailable: true, sameStartupIdRetried: true, oneBoundStartup: true, oneThread: true, oneWorktree: true, oneQueuedFirstTurn: true, onePersistedPrompt: true };
}

function hasOneWorktreeSetupThread(threads, threadId) {
return Array.isArray(threads) && threads.length === 1 && threads[0]?.id === threadId;
}

function hasOneBoundWorktreeSetup(startups, record) {
return Array.isArray(startups?.records) && startups.records.length === 1
&& startups.records[0]?.startupId === record.startupId
&& startups.records[0]?.threadId === record.threadId;
}

function hasOnePersistedWorktreeSetupPrompt(result, messageId, prompt) {
return Array.isArray(result?.messages) && result.messages.length === 1 && result.hasMore === false
&& result.messages[0]?.id === messageId && result.messages[0]?.role === "user"
&& result.messages[0]?.content === prompt;
}

function recordWorktreeSetupThread(evidenceDirectory, record, thread) {
record.threadId = thread.id;
record.worktreePath = thread.worktree_path;
Expand Down Expand Up @@ -1039,6 +1112,7 @@ function writeWorktreeSetupReceipt(repoRoot, receiptPath, report) {
const receipt = {
ok: report.failure === null && report.cleanup.failure === null,
command: report.command,
lostResponse: report.lostResponse,
checkout: report.checkout,
cancellation: report.cancellation,
cleanup: report.cleanup,
Expand Down Expand Up @@ -2265,10 +2339,25 @@ async function waitForSocketOpen(ws, state, pending) {
function createSocketClient(ws, state, pending) {
return {
rpc: (method, params, deadline) => sendSocketRpc(ws, state, pending, method, params, deadline),
sendWithoutResponse: (method, params) => sendSocketRpcWithoutResponse(ws, state, method, params),
close: () => closeSocketClient(ws, state, pending),
};
}

function sendSocketRpcWithoutResponse(ws, state, method, params) {
if (state.failure) return Promise.reject(state.failure);
if (ws.readyState !== state.openState) {
return Promise.reject(socketFailure("The WebSocket is not open", "Run bun .agents/skills/verify-mcode/scripts/verify-mcode.mjs runtime health, then retry the command."));
}
const id = `verify-${++state.counter}`;
return new Promise((resolve, reject) => {
ws.send(JSON.stringify({ id, method, params }), (error) => {
if (error) reject(socketFailure(`RPC ${method} could not be sent`, "Run bun .agents/skills/verify-mcode/scripts/verify-mcode.mjs runtime health, then retry the command."));
else resolve();
});
});
}

function sendSocketRpc(ws, state, pending, method, params, deadline) {
if (state.failure) return Promise.reject(state.failure);
if (ws.readyState !== state.openState) {
Expand Down
23 changes: 23 additions & 0 deletions .agents/skills/verify-mcode/scripts/runtime.test.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as NodeAssertStrict from "node:assert/strict";
import * as NodeChildProcess from "node:child_process";
import * as NodeFS from "node:fs";
import * as NodeModule from "node:module";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";
import * as NodeTest from "node:test";
Expand Down Expand Up @@ -149,6 +150,28 @@ NodeTest.test("rejects desktop verification sockets without loopback authenticat
await NodeAssertStrict.rejects(openVerificationSocketUrl(process.cwd(), "ws://localhost/"), /lacks its authentication token/);
});

NodeTest.test("lost RPC response does not satisfy the next request on the same socket", async () => {
const serverRequire = NodeModule.createRequire(NodePath.join(process.cwd(), "apps", "server", "package.json"));
const { WebSocketServer } = serverRequire("ws");
const server = new WebSocketServer({ host: "127.0.0.1", port: 0 });
let socket;
try {
await new Promise((resolve) => server.once("listening", resolve));
server.on("connection", (connection) => {
connection.on("message", (raw) => {
const request = JSON.parse(raw.toString());
connection.send(JSON.stringify({ id: request.id, result: request.method }));
});
});
socket = await openVerificationSocketUrl(process.cwd(), `ws://127.0.0.1:${server.address().port}/?token=fixture`);
await socket.sendWithoutResponse("agent.createAndSend", { startupId: "fixture" });
NodeAssertStrict.equal(await socket.rpc("thread.startup.get", { startupId: "fixture" }), "thread.startup.get");
} finally {
await socket?.close();
await new Promise((resolve) => server.close(resolve));
}
});

function writeTimestampedFile(path, modifiedMs) {
NodeFS.mkdirSync(NodePath.dirname(path), { recursive: true });
NodeFS.writeFileSync(path, "fixture\n");
Expand Down
1 change: 1 addition & 0 deletions apps/server/drizzle/0064_fuzzy_jimmy_woo.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE `thread_startups` ADD `request_fingerprint` text;
Loading
Loading