@agentswf/engine 0.0.0-stage → 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +39 -4
- package/src/accounting/format.ts +252 -0
- package/src/accounting/plans.ts +38 -0
- package/src/accounting/prices.ts +98 -0
- package/src/accounting/summary.ts +385 -0
- package/src/accounting/tokens.ts +34 -0
- package/src/agent-launcher.ts +127 -0
- package/src/allowance-command.ts +105 -0
- package/src/attempt-ending.ts +198 -0
- package/src/attempt-output.ts +132 -0
- package/src/attempt-view.ts +362 -0
- package/src/canonical-json.ts +12 -0
- package/src/control-plane.ts +424 -0
- package/src/deadlines.ts +106 -0
- package/src/decisions/directory.ts +432 -0
- package/src/decisions/fake.ts +60 -0
- package/src/decisions/openrouter.ts +161 -0
- package/src/decisions/seam.ts +70 -0
- package/src/display-path.ts +16 -0
- package/src/duration.ts +12 -0
- package/src/errors.ts +4 -0
- package/src/files.ts +78 -0
- package/src/herdr-placement.ts +182 -0
- package/src/herdr-run-session.ts +530 -0
- package/src/herdr-workspace-marks.ts +194 -0
- package/src/here.ts +289 -0
- package/src/index.ts +5 -0
- package/src/jsonl.ts +74 -0
- package/src/logs-command.ts +605 -0
- package/src/machine.ts +27 -0
- package/src/message-operation.ts +245 -0
- package/src/messaging/limits.ts +35 -0
- package/src/messaging/messaging.ts +1303 -0
- package/src/messaging/routes.ts +79 -0
- package/src/messaging/text.ts +251 -0
- package/src/operation-events.ts +182 -0
- package/src/operation-liveness.ts +674 -0
- package/src/operator-aliases.ts +10 -0
- package/src/operator-cli.ts +569 -0
- package/src/operator-runtime.ts +503 -0
- package/src/pane-layout.ts +125 -0
- package/src/progress-view.ts +568 -0
- package/src/result-slots.ts +325 -0
- package/src/result-validation.ts +47 -0
- package/src/run-command.ts +241 -0
- package/src/run-commands.ts +215 -0
- package/src/run-event-log.ts +72 -0
- package/src/run-prepare.ts +178 -0
- package/src/run-progress.ts +137 -0
- package/src/run-read.ts +425 -0
- package/src/run-state.ts +120 -0
- package/src/run-usage.ts +393 -0
- package/src/run-view.ts +225 -0
- package/src/runs-command.ts +356 -0
- package/src/runs.ts +645 -0
- package/src/sandbox-homes.ts +211 -0
- package/src/sandboxes.ts +516 -0
- package/src/skills/fetch.ts +305 -0
- package/src/skills/run-skills.ts +181 -0
- package/src/skills/sources.ts +87 -0
- package/src/skills/tree.ts +60 -0
- package/src/stage-ledger.ts +535 -0
- package/src/stage-plan.ts +127 -0
- package/src/stage-schema.ts +88 -0
- package/src/stopped.ts +36 -0
- package/src/test-command.ts +110 -0
- package/src/testing/operation-events.ts +89 -0
- package/src/testing.ts +164 -0
- package/src/workflow-loader.ts +113 -0
- package/src/workflow-runner.ts +3665 -0
- package/src/workflow-test-preload.ts +13 -0
- package/src/workflow-testing/decisions.ts +129 -0
- package/src/workflow-testing/host.ts +811 -0
- package/src/workflow-testing/index.ts +420 -0
- package/src/workflow-testing/script.ts +447 -0
- package/src/workflow-testing/workflow-testing.typecheck.ts +47 -0
- package/README.md +0 -3
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Money } from "@agentswf/contract/records";
|
|
2
|
+
import type { JsonObject, JsonValue, Question } from "@agentswf/contract/workflow";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A question's distribution, as awf shapes it, before the picks are derived: a choice's by option
|
|
6
|
+
* name, a score's by level index.
|
|
7
|
+
*/
|
|
8
|
+
export type ProviderAnswer =
|
|
9
|
+
| { type: "choice"; probabilities: Record<string, number> }
|
|
10
|
+
| { type: "score"; probabilities: number[] }
|
|
11
|
+
| { type: "yes-no"; yes: number };
|
|
12
|
+
|
|
13
|
+
export type ProviderRequest = {
|
|
14
|
+
/** The call's key, as the workflow named it. */
|
|
15
|
+
key: string;
|
|
16
|
+
model: string;
|
|
17
|
+
state: string | JsonObject | JsonValue[];
|
|
18
|
+
questions: Record<string, Question>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ProviderResponse = {
|
|
22
|
+
/** The versioned model that answered. */
|
|
23
|
+
snapshot: string;
|
|
24
|
+
answers: Record<string, ProviderAnswer>;
|
|
25
|
+
/** Absent when the provider did not report them: unknown, not zero. */
|
|
26
|
+
tokens?: { input: number; output: number };
|
|
27
|
+
charged?: Money;
|
|
28
|
+
requestId?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* One kind of decision model behind one API. It sends one request and translates both ways; the
|
|
33
|
+
* directory owns deadlines, retries and records.
|
|
34
|
+
*/
|
|
35
|
+
export interface DecisionProvider {
|
|
36
|
+
decide(request: ProviderRequest, signal: AbortSignal): Promise<ProviderResponse>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A request the provider refused or could not complete. Whatever it reported spending is kept. */
|
|
40
|
+
export class DecisionProviderError extends Error {
|
|
41
|
+
readonly retryable: boolean;
|
|
42
|
+
readonly tokens?: { input: number; output: number };
|
|
43
|
+
readonly charged?: Money;
|
|
44
|
+
readonly requestId?: string;
|
|
45
|
+
|
|
46
|
+
constructor(
|
|
47
|
+
message: string,
|
|
48
|
+
options: {
|
|
49
|
+
retryable: boolean;
|
|
50
|
+
tokens?: { input: number; output: number };
|
|
51
|
+
charged?: Money;
|
|
52
|
+
requestId?: string;
|
|
53
|
+
},
|
|
54
|
+
) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.name = "DecisionProviderError";
|
|
57
|
+
this.retryable = options.retryable;
|
|
58
|
+
if (options.tokens) this.tokens = options.tokens;
|
|
59
|
+
if (options.charged) this.charged = options.charged;
|
|
60
|
+
if (options.requestId) this.requestId = options.requestId;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** What the operator runtime installed: providers by name, and the aliases workflows ask for. */
|
|
65
|
+
export type DecisionInstallation = {
|
|
66
|
+
providers: Readonly<Record<string, DecisionProvider>>;
|
|
67
|
+
aliases: Readonly<Record<string, { provider: string; model: string }>>;
|
|
68
|
+
/** Aliases that could not be installed, and why, so asking for one says what to fix. */
|
|
69
|
+
unavailable?: Readonly<Record<string, string>>;
|
|
70
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `path` as typed from `cwd`: relative under it, absolute elsewhere, so it works from anywhere; with
|
|
5
|
+
* `home`, one under that home folder from `~`, shorter to read, though a quoted shell word won't
|
|
6
|
+
* expand it.
|
|
7
|
+
*/
|
|
8
|
+
export function pathFrom(cwd: string, path: string, options: { home?: string } = {}): string {
|
|
9
|
+
const absolute = resolve(cwd, path);
|
|
10
|
+
const inside = relative(cwd, absolute);
|
|
11
|
+
if (inside && !inside.startsWith("..") && !isAbsolute(inside)) return inside;
|
|
12
|
+
const { home } = options;
|
|
13
|
+
return home !== undefined && absolute.startsWith(`${home}/`)
|
|
14
|
+
? `~${absolute.slice(home.length)}`
|
|
15
|
+
: absolute;
|
|
16
|
+
}
|
package/src/duration.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** A duration such as `500ms`, `30s`, `20m` or `2h`, in milliseconds. */
|
|
2
|
+
export function parseDuration(value: string): number {
|
|
3
|
+
const matched = /^(\d+)(ms|s|m|h)$/.exec(value);
|
|
4
|
+
if (!matched) throw new Error(`invalid duration: ${value}`);
|
|
5
|
+
const amount = Number(matched[1]);
|
|
6
|
+
if (!Number.isSafeInteger(amount) || amount <= 0) throw new Error(`invalid duration: ${value}`);
|
|
7
|
+
const unit = matched[2];
|
|
8
|
+
const multiplier = unit === "ms" ? 1 : unit === "s" ? 1_000 : unit === "m" ? 60_000 : 3_600_000;
|
|
9
|
+
const milliseconds = amount * multiplier;
|
|
10
|
+
if (!Number.isSafeInteger(milliseconds)) throw new Error(`duration is too large: ${value}`);
|
|
11
|
+
return milliseconds;
|
|
12
|
+
}
|
package/src/errors.ts
ADDED
package/src/files.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { link, lstat, open, rename, unlink } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Writes `value` to `path` whole: a temp file beside it, synced, then renamed into place. A crash
|
|
7
|
+
* leaves the old file or the new one, never half of either.
|
|
8
|
+
*/
|
|
9
|
+
export async function writeJson(path: string, value: unknown): Promise<void> {
|
|
10
|
+
await writeWhole(path, jsonText(value));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** As `writeJson`, for text. */
|
|
14
|
+
export async function writeWhole(path: string, text: string): Promise<void> {
|
|
15
|
+
const temporary = temporaryBeside(path);
|
|
16
|
+
try {
|
|
17
|
+
await writeSynced(temporary, text);
|
|
18
|
+
await rename(temporary, path);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
await unlink(temporary).catch(() => undefined);
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Links a complete file holding `value` to `path`, which fails when `path` exists: the first to
|
|
27
|
+
* link holds it. False when it was taken.
|
|
28
|
+
*/
|
|
29
|
+
export async function linkNew(path: string, value: unknown): Promise<boolean> {
|
|
30
|
+
const temporary = temporaryBeside(path);
|
|
31
|
+
try {
|
|
32
|
+
await writeSynced(temporary, jsonText(value));
|
|
33
|
+
await link(temporary, path);
|
|
34
|
+
return true;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (isCode(error, "EEXIST")) return false;
|
|
37
|
+
throw error;
|
|
38
|
+
} finally {
|
|
39
|
+
// The linked inode remains; one left behind is a private temp file a sweep removes.
|
|
40
|
+
await unlink(temporary).catch(() => undefined);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Creates `path`, which must not exist, holding `text`, synced. */
|
|
45
|
+
async function writeSynced(path: string, text: string): Promise<void> {
|
|
46
|
+
const handle = await open(path, "wx", 0o600);
|
|
47
|
+
try {
|
|
48
|
+
await handle.writeFile(text);
|
|
49
|
+
await handle.sync();
|
|
50
|
+
} finally {
|
|
51
|
+
await handle.close();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Whether `path` names anything, a dangling link included. */
|
|
56
|
+
export async function exists(path: string): Promise<boolean> {
|
|
57
|
+
return lstat(path).then(
|
|
58
|
+
() => true,
|
|
59
|
+
() => false,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Whether `error` is a system error with one of `codes`, such as `ENOENT`. */
|
|
64
|
+
export function isCode(error: unknown, ...codes: string[]): boolean {
|
|
65
|
+
return (
|
|
66
|
+
error instanceof Error &&
|
|
67
|
+
"code" in error &&
|
|
68
|
+
codes.includes(String((error as { code: unknown }).code))
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function jsonText(value: unknown): string {
|
|
73
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function temporaryBeside(path: string): string {
|
|
77
|
+
return join(dirname(path), `.tmp-${randomBytes(6).toString("hex")}`);
|
|
78
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, rename, rmdir, stat } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { HERDR_VERSION, type RunProcess } from "@agentswf/harness";
|
|
5
|
+
import { messageOf } from "./errors";
|
|
6
|
+
import { ensureRunSession, parseSessions, type RunSessionDeps } from "./herdr-run-session";
|
|
7
|
+
import { machinePaths } from "./machine";
|
|
8
|
+
|
|
9
|
+
/** Where a layout may open a session awf does not otherwise use: one named `awf-…` it starts. */
|
|
10
|
+
const OWN_SESSION = /^awf(-|$)/;
|
|
11
|
+
const LOCK_POLL_MS = 100;
|
|
12
|
+
/** Held inside a session's queue: a short wait, then made anyway. */
|
|
13
|
+
const LOCK_WAIT_MS = 5_000;
|
|
14
|
+
/** A lock older than this was left by a run that died holding it. */
|
|
15
|
+
const LOCK_STALE_MS = 30_000;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Whether a session a layout names can be used: undefined when it can, else why not. One named
|
|
19
|
+
* `awf-…` is started, swept and restarted as the run session is; any other must be running. Either
|
|
20
|
+
* must answer, on the Herdr the adapter drives.
|
|
21
|
+
*/
|
|
22
|
+
export async function namedSession(
|
|
23
|
+
name: string,
|
|
24
|
+
deps: RunSessionDeps,
|
|
25
|
+
): Promise<string | undefined> {
|
|
26
|
+
if (OWN_SESSION.test(name)) {
|
|
27
|
+
try {
|
|
28
|
+
await ensureRunSession(name, deps);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return messageOf(error);
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
const listed = await deps.run({
|
|
34
|
+
argv: ["herdr", "session", "list", "--json"],
|
|
35
|
+
timeoutMs: 10_000,
|
|
36
|
+
});
|
|
37
|
+
const found = parseSessions(listed.stdout).find((session) => session.name === name);
|
|
38
|
+
if (!found?.running) {
|
|
39
|
+
return "it is not running, and awf starts only sessions named awf-…";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return wrongVersion(name, deps.run);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Why the session's server is not the Herdr the adapter drives; undefined when it is. */
|
|
46
|
+
async function wrongVersion(session: string, run: RunProcess): Promise<string | undefined> {
|
|
47
|
+
const status = await run({
|
|
48
|
+
argv: ["herdr", "--session", session, "status", "server", "--json"],
|
|
49
|
+
timeoutMs: 10_000,
|
|
50
|
+
});
|
|
51
|
+
if (status.exitCode !== 0) return "it does not answer";
|
|
52
|
+
let version: unknown;
|
|
53
|
+
try {
|
|
54
|
+
version = JSON.parse(status.stdout).version;
|
|
55
|
+
} catch {
|
|
56
|
+
return "its status could not be read";
|
|
57
|
+
}
|
|
58
|
+
const driven = HERDR_VERSION.replace(/^herdr /, "");
|
|
59
|
+
return version === driven ? undefined : `it runs Herdr ${String(version)}, not ${driven}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type OriginDeps = {
|
|
63
|
+
run: RunProcess;
|
|
64
|
+
environment: Readonly<Record<string, string | undefined>>;
|
|
65
|
+
/** The run session, which `"origin"` must not be. */
|
|
66
|
+
runSession: string;
|
|
67
|
+
/** The calling session's pane under `--here`, found before the workflow started. */
|
|
68
|
+
callerPane?: string;
|
|
69
|
+
/** This process, whose ancestors include the shell `awf run` was typed in. */
|
|
70
|
+
pid?: number;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The workspace `awf run` was typed in, and its session; why not, where it can't be used. Read
|
|
75
|
+
* from Herdr, never from `HERDR_WORKSPACE_ID`, which a codex shell inherits from its daemon (E8).
|
|
76
|
+
*/
|
|
77
|
+
export async function findOrigin(
|
|
78
|
+
deps: OriginDeps,
|
|
79
|
+
): Promise<{ session: string; workspaceId: string } | string> {
|
|
80
|
+
const { run, environment } = deps;
|
|
81
|
+
const socket = environment.HERDR_SOCKET_PATH;
|
|
82
|
+
if (!socket) return "awf run was not started in a Herdr pane";
|
|
83
|
+
const listed = await run({ argv: ["herdr", "session", "list", "--json"], timeoutMs: 10_000 });
|
|
84
|
+
const session = parseSessions(listed.stdout).find((found) => found.socketPath === socket)?.name;
|
|
85
|
+
if (!session) return `no Herdr session owns ${socket}`;
|
|
86
|
+
if (session === deps.runSession) {
|
|
87
|
+
return `it is the run session, ${session}, whose workspaces are runs'`;
|
|
88
|
+
}
|
|
89
|
+
const herdr = (args: string[]) =>
|
|
90
|
+
run({ argv: ["herdr", "--session", session, ...args], timeoutMs: 10_000 });
|
|
91
|
+
const stale = await wrongVersion(session, run);
|
|
92
|
+
if (stale) return `its session ${session}: ${stale}`;
|
|
93
|
+
let paneId = deps.callerPane;
|
|
94
|
+
if (!paneId) {
|
|
95
|
+
paneId = environment.HERDR_PANE_ID;
|
|
96
|
+
if (!paneId) return "HERDR_PANE_ID is not set";
|
|
97
|
+
// A codex shell inherits its daemon's HERDR_PANE_ID, which names another pane (E8).
|
|
98
|
+
const info = await herdr(["pane", "process-info", "--pane", paneId]);
|
|
99
|
+
const shell = info.exitCode === 0 ? readShell(info.stdout) : undefined;
|
|
100
|
+
if (shell === undefined) return `pane ${paneId} could not be read`;
|
|
101
|
+
if (!(await ancestors(run, deps.pid ?? process.pid)).includes(shell)) {
|
|
102
|
+
return `HERDR_PANE_ID names pane ${paneId}, which awf run was not typed in`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const got = await herdr(["pane", "get", paneId]);
|
|
106
|
+
let workspaceId: unknown;
|
|
107
|
+
try {
|
|
108
|
+
workspaceId = JSON.parse(got.stdout).result?.pane?.workspace_id;
|
|
109
|
+
} catch {}
|
|
110
|
+
if (got.exitCode !== 0 || typeof workspaceId !== "string") {
|
|
111
|
+
return `pane ${paneId}'s workspace could not be read`;
|
|
112
|
+
}
|
|
113
|
+
return { session, workspaceId };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readShell(stdout: string): number | undefined {
|
|
117
|
+
try {
|
|
118
|
+
const shell = JSON.parse(stdout).result?.process_info?.shell_pid;
|
|
119
|
+
return typeof shell === "number" ? shell : undefined;
|
|
120
|
+
} catch {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** `pid`'s parent, its parent's, and so on, as `ps` gives them. */
|
|
126
|
+
async function ancestors(run: RunProcess, pid: number): Promise<number[]> {
|
|
127
|
+
const found: number[] = [];
|
|
128
|
+
for (let current = pid; current > 1 && found.length < 64; ) {
|
|
129
|
+
const parent = await run({
|
|
130
|
+
argv: ["ps", "-o", "ppid=", "-p", String(current)],
|
|
131
|
+
timeoutMs: 5_000,
|
|
132
|
+
});
|
|
133
|
+
const next = Number(parent.stdout.trim());
|
|
134
|
+
if (parent.exitCode !== 0 || !Number.isInteger(next) || next <= 0) break;
|
|
135
|
+
found.push(next);
|
|
136
|
+
current = next;
|
|
137
|
+
}
|
|
138
|
+
return found;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Holds `name` in `session` against other runs while its workspace is looked for and made, so two
|
|
143
|
+
* runs make one. A directory, made or not in one step; one left by a run that died expires.
|
|
144
|
+
*/
|
|
145
|
+
export function workspaceLock(home: string) {
|
|
146
|
+
return async (session: string, name: string): Promise<() => Promise<void>> => {
|
|
147
|
+
const digest = createHash("sha256").update(name).digest("hex").slice(0, 16);
|
|
148
|
+
const dir = join(machinePaths(home).herdr, "locks", session);
|
|
149
|
+
const lock = join(dir, digest);
|
|
150
|
+
await mkdir(dir, { recursive: true });
|
|
151
|
+
const by = Date.now() + LOCK_WAIT_MS;
|
|
152
|
+
for (;;) {
|
|
153
|
+
try {
|
|
154
|
+
await mkdir(lock);
|
|
155
|
+
return () => rmdir(lock).catch(() => undefined);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
158
|
+
}
|
|
159
|
+
const made = await stat(lock).then(
|
|
160
|
+
(found) => found.mtimeMs,
|
|
161
|
+
() => undefined,
|
|
162
|
+
);
|
|
163
|
+
if (made !== undefined && Date.now() - made > LOCK_STALE_MS) {
|
|
164
|
+
// Moved aside first: of two waiters that saw it stale, only one moves it, and neither
|
|
165
|
+
// removes a lock the other has just taken.
|
|
166
|
+
const aside = `${lock}.${process.pid}.${Date.now()}`;
|
|
167
|
+
if (
|
|
168
|
+
await rename(lock, aside).then(
|
|
169
|
+
() => true,
|
|
170
|
+
() => false,
|
|
171
|
+
)
|
|
172
|
+
) {
|
|
173
|
+
await rmdir(aside).catch(() => undefined);
|
|
174
|
+
}
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
// Taking it anyway makes at worst a second workspace of the name, which is still usable.
|
|
178
|
+
if (Date.now() >= by) return async () => undefined;
|
|
179
|
+
await Bun.sleep(LOCK_POLL_MS);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|