[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
/**
* Per-session execution queue for tool calls that target the same terminal
* session.
*
* Background — issue #1101 problem 3:
*
* Vercel AI SDK dispatches every tool_use block emitted in one assistant
* turn through `Promise.all(toolCalls.map(execute))`, so an LLM that asks
* for three commands "at once" sends three simultaneous `bridge.aiExec()`
* calls at the underlying PTY. The main-process session mutex
* (`mcpServerBridge.reserveSessionExecution`) only lets one through and
* rejects the rest with `{ ok: false, error: "Session already has another
* command in progress..." }`. The LLM then sees two synthetic errors plus
* one real result for a turn it expected to be all-or-nothing — and the
* Anthropic API has occasionally rejected the resulting trace with a
* `tool_use ids were found without tool_result blocks` 400.
*
* The cleanest fix is to never let those calls race in the first place:
* serialize at the renderer-side tool execute boundary so the bridge sees
* one command per session at a time. The bridge mutex stays as
* defense-in-depth for non-LLM IPC paths (terminal_start, MCP, etc.).
*
* The queue exposes both a high-level `chainBySessionKey(key, task)` for
* simple "run this when it's our turn" callers, and a lower-level
* `reserveSessionSlot(key)` for callers that need to do non-blocking work
* (e.g. await an approval prompt) *while* their queue slot is held — so
* the queue order matches the LLM's emission order independent of when
* each call's approval lands.
*/
const queues = new Map<string, Promise<unknown>>();
/**
* A reserved slot in a session's execution queue. The slot is added to
* the queue tail synchronously when {@link reserveSessionSlot} is called,
* so call order is fixed at reservation time — regardless of how long
* each caller spends on pre-work (approval prompts, abort checks, etc.)
* before they actually start.
*
* Lifecycle:
* 1. `reserveSessionSlot(key)` — synchronously snaps a place in line.
* 2. caller does whatever pre-work they want, in parallel with siblings.
* 3. `await slot.ready` — blocks until the previous slot has released.
* 4. caller does the serialized work.
* 5. `slot.release()` — frees the next slot. Idempotent.
*
* The slot **must** be released exactly once (typically from a `finally`)
* even if the caller decides to skip the serialized work — otherwise
* subsequent slots queued behind it never start.
*/
export interface SessionExecutionSlot {
/** Resolves when this slot is at the head of its queue. */
readonly ready: Promise<void>;
/** Releases this slot. Safe to call multiple times. */
release(): void;
}
export function reserveSessionSlot(key: string): SessionExecutionSlot {
const prev = queues.get(key) ?? Promise.resolve();
let resolveDone!: () => void;
const done = new Promise<void>((r) => {
resolveDone = r;
});
// The new tail of this key's queue: previous tail → our `done`.
// Wrap in a non-rejecting chain so a thrown task never poisons later
// callers waiting on this tail.
const tail: Promise<unknown> = prev.then(() => done).catch(() => undefined);
queues.set(key, tail);
// Best-effort cleanup once we're the last in line — keeps the map
// from growing without bound across many short-lived sessions. A
// later caller that arrived between `queues.set` and this finally
// will already have replaced the tail; we only clear when we're
// still it.
void tail.finally(() => {
if (queues.get(key) === tail) {
queues.delete(key);
}
});
let released = false;
return {
ready: prev.then(
() => undefined,
() => undefined,
),
release(): void {
if (released) return;
released = true;
resolveDone();
},
};
}
/**
* Run `task` after every previously-reserved slot with the same `key`
* has released. Returns the task's resolved value (or rejects if the
* task throws). A failure in one task does not poison the queue head
* for subsequent callers — the chain only waits on settlement, not
* success.
*
* For callers that need to interleave pre-work with the queue wait
* (e.g. approval prompts that should run in parallel even though the
* actual command must run serially), use {@link reserveSessionSlot}
* directly.
*/
export async function chainBySessionKey<T>(key: string, task: () => Promise<T>): Promise<T> {
const slot = reserveSessionSlot(key);
try {
await slot.ready;
return await task();
} finally {
slot.release();
}
}
/** Test-only: inspect the live queue. */
export function getSessionExecutionQueueSizeForTests(): number {
return queues.size;
}
/** Test-only: drop all queued work. */
export function resetSessionExecutionQueueForTests(): void {
queues.clear();
}