mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
p2 (Pi Mobile on Chord v2) moves from pi-private into pi as one commit. Root scripts that loop all workspaces use --if-present; npm test, ./test.sh and the release script cover packages/* only (p2: cd apps/p2 && npm test); sync-versions also updates apps' workspace pins.
84 lines
3.1 KiB
TypeScript
84 lines
3.1 KiB
TypeScript
// The codemode tool calls running in this worker, so that removing the plugin can end them.
|
|
|
|
import {
|
|
type Harness as DurableHarness,
|
|
LiveDoc,
|
|
type TaskId,
|
|
type ToolExecutionApi,
|
|
type ToolExecutionResult,
|
|
} from "@earendil-works/pi-durable";
|
|
import type { Context } from "@p2/app/v1";
|
|
import { BACKGROUND_CONTEXT } from "@p2/app/v1";
|
|
|
|
/** What a call removed mid-script reports, as its output, before Durable settles it aborted. */
|
|
export const REMOVED_MESSAGE = "Code mode was removed while this script ran.";
|
|
/** How long to wait for that output to be committed to the call's slot before aborting anyway. */
|
|
const REPORT_MS = 2000;
|
|
|
|
/**
|
|
* Problem: removing the plugin by a hot reload closes the sandbox while the Harness stays open. Durable stores no
|
|
* outcome for a run the sandbox ended on its own (so a restart replays it), and the call then waits for its own
|
|
* cancellation: nothing would ever settle it.
|
|
* Solution: on removal (not on a worker stop, after which the next worker replays), each running call reports why in
|
|
* its output, then is aborted through Durable (`abortTask`), which ends its script and nested calls bottom-up and
|
|
* settles it with an error result. Only then is the sandbox closed.
|
|
*/
|
|
export class RunningCalls {
|
|
readonly #calls = new Map<TaskId, ToolExecutionApi>();
|
|
readonly #idle = new Set<() => void>();
|
|
|
|
/** Runs one codemode `execute()`, known as running until it returns. */
|
|
async track(api: ToolExecutionApi, run: () => Promise<ToolExecutionResult>): Promise<ToolExecutionResult> {
|
|
this.#calls.set(api.taskId, api);
|
|
try {
|
|
return await run();
|
|
} finally {
|
|
this.#calls.delete(api.taskId);
|
|
if (this.#calls.size === 0) for (const resolve of this.#idle) resolve();
|
|
}
|
|
}
|
|
|
|
/** Resolves once no call runs (at once when none does). */
|
|
idle(): Promise<void> {
|
|
if (this.#calls.size === 0) return Promise.resolve();
|
|
return new Promise((resolve) => {
|
|
const done = () => {
|
|
this.#idle.delete(done);
|
|
resolve();
|
|
};
|
|
this.#idle.add(done);
|
|
});
|
|
}
|
|
|
|
get size(): number {
|
|
return this.#calls.size;
|
|
}
|
|
|
|
/** Ends every running call with an error result; resolves once each is terminal. */
|
|
async cancel(harness: DurableHarness): Promise<void> {
|
|
const context = BACKGROUND_CONTEXT;
|
|
await Promise.all(
|
|
[...this.#calls.values()].map(async (api) => {
|
|
try {
|
|
api.output(`${REMOVED_MESSAGE}\n`);
|
|
await reported(harness, api, context);
|
|
} catch {
|
|
// Already settling: the abort below decides.
|
|
}
|
|
await harness.abortTask(api.taskId, context);
|
|
await harness.waitForTask(api.taskId, context);
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
/** Resolves once the call's slot holds the removal message (the output is committed with a throttle), or after a bound. */
|
|
async function reported(harness: DurableHarness, api: ToolExecutionApi, context: Context): Promise<void> {
|
|
const deadline = Date.now() + REPORT_MS;
|
|
while (Date.now() < deadline) {
|
|
const live = await harness.snapshot(LiveDoc, api.conversationId, context);
|
|
const slot = live?.tools?.find((tool) => tool.taskId === api.taskId);
|
|
if (!slot || slot.status === "done" || slot.output?.includes(REMOVED_MESSAGE)) return;
|
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
}
|
|
}
|