Compare commits

..

46 Commits

Author SHA1 Message Date
Vincent Koc
568c7c729a docs: clarify native node approval docs 2026-05-31 23:36:15 +02:00
Vincent Koc
404bce7caa fix(cli): verify loopback password auth 2026-05-31 23:36:15 +02:00
Vincent Koc
102967bcc9 fix(protocol): model native node approvals 2026-05-31 23:36:14 +02:00
Vincent Koc
eb00a1e5b6 test(ui): normalize vite resolve hook 2026-05-31 23:36:14 +02:00
Vincent Koc
80a2a7f7f0 test: remove stale unsafe assertion suppressions 2026-05-31 23:36:14 +02:00
Vincent Koc
6fa8e695a8 test(cli): remove stale lint suppressions 2026-05-31 23:36:14 +02:00
Vincent Koc
1356b27198 fix(gateway): remove stale native approval type 2026-05-31 23:36:14 +02:00
Vincent Koc
54b60c838d fix(cli): repair Windows node typecheck 2026-05-31 23:36:14 +02:00
Vincent Koc
6768cd33fd fix(cli): repair Windows node validation 2026-05-31 23:36:14 +02:00
Vincent Koc
a286d7262e fix(protocol): reject empty native approval rules 2026-05-31 23:36:14 +02:00
Vincent Koc
c4be467cf0 test(cli): complete program gateway mock 2026-05-31 23:36:14 +02:00
Vincent Koc
0fc2bce669 fix(build): run TypeScript helpers through tsx 2026-05-31 23:36:14 +02:00
Vincent Koc
52ae286a9a fix(cli): clarify node shell execution guidance 2026-05-31 23:36:13 +02:00
Vincent Koc
2fea1837eb fix(ui): render native node approvals read-only 2026-05-31 23:36:13 +02:00
Vincent Koc
4bb4d9bd01 fix(cli): satisfy Windows node approval lint 2026-05-31 23:36:13 +02:00
Vincent Koc
fa41a77c5e fix(cli): resolve gateway token refs before direct auth 2026-05-31 23:36:13 +02:00
Vincent Koc
82cffcbf32 test(cli): keep gateway call mocks complete 2026-05-31 23:36:13 +02:00
Vincent Koc
acf1b75f8b fix(cli): hide transient commandless node rows 2026-05-31 23:36:13 +02:00
Vincent Koc
f614938006 fix(gateway): probe explicit shared tokens best-effort 2026-05-31 23:36:13 +02:00
Vincent Koc
eef48732a0 fix(gateway): preserve stable node event identity 2026-05-31 23:36:13 +02:00
Vincent Koc
aa0e3e7fa7 test(gateway): mark direct scoped call shared 2026-05-31 23:36:13 +02:00
Vincent Koc
85fb433392 fix(gateway): require proven shared explicit tokens 2026-05-31 23:36:13 +02:00
Vincent Koc
c62866a292 test(cli): isolate device auth environment 2026-05-31 23:36:13 +02:00
Vincent Koc
a91cc0e8d0 test(cli): keep direct auth URL probing optional 2026-05-31 23:36:12 +02:00
Vincent Koc
4e75e6677c fix(cli): detect implicit shared loopback auth 2026-05-31 23:36:12 +02:00
Vincent Koc
5ddfccd08d fix(gateway): merge stable node catalog rows 2026-05-31 23:36:12 +02:00
Vincent Koc
d09f1e9e98 fix(gateway): reject unbound stable node spoofing 2026-05-31 23:36:12 +02:00
Vincent Koc
31157b6386 fix(gateway): gate node exec approval RPCs 2026-05-31 23:36:12 +02:00
Vincent Koc
6a7d095a47 fix(gateway): tolerate unresolved auth probes 2026-05-31 23:36:12 +02:00
Vincent Koc
30d8c0bd59 fix(protocol): carry node pairing device id 2026-05-31 23:36:12 +02:00
Vincent Koc
95f955b8e0 fix(gateway): resolve secret-backed loopback tokens 2026-05-31 23:36:12 +02:00
Vincent Koc
4e931ef4a6 fix(gateway): bind unbound stable node approvals 2026-05-31 23:36:12 +02:00
Vincent Koc
c901c12f3e fix(cli): preserve device-token loopback auth 2026-05-31 23:36:12 +02:00
Vincent Koc
0abd9d5658 fix(cli): preserve empty native approval rules 2026-05-31 23:36:12 +02:00
Vincent Koc
4c670ef3c9 test(cli): keep gateway call mock complete 2026-05-31 23:36:11 +02:00
Vincent Koc
db55387598 fix(gateway): isolate rejected stable pairing metadata 2026-05-31 23:36:11 +02:00
Vincent Koc
3a36b695eb fix(protocol): reject empty native exec approval policies 2026-05-31 23:36:11 +02:00
Vincent Koc
8ff7e53bf0 fix(gateway): narrow node adoption device id 2026-05-31 23:36:11 +02:00
Vincent Koc
0e0dc93053 fix(auth): discard stale device token scopes 2026-05-31 23:36:11 +02:00
Vincent Koc
602b763c1d fix(gateway): bind node pairing to device identity 2026-05-31 23:36:11 +02:00
Vincent Koc
6bd53b2072 test(tui): tighten command handler mock types 2026-05-31 23:36:11 +02:00
Vincent Koc
45c9ba7866 fix(gateway): expose Windows exec approval node commands 2026-05-31 23:36:11 +02:00
Vincent Koc
1f6e89e307 fix(cli): support host-native node exec approvals 2026-05-31 23:36:11 +02:00
Vincent Koc
f6f89a1a96 test(cli): use documentation IP in device auth test 2026-05-31 23:36:10 +02:00
Vincent Koc
a3b6afdcb6 fix(cli): harden Windows node gateway commands 2026-05-31 23:36:10 +02:00
Vincent Koc
53a474e503 fix(pairing): preserve stable device tokens on metadata refresh 2026-05-31 23:36:10 +02:00
980 changed files with 8922 additions and 21309 deletions

View File

@@ -4,11 +4,11 @@ profile: openclaw-check
provider: azure
class: standard
capacity:
market: on-demand
market: spot
strategy: most-available
# The Azure-backed billing account carries the OpenClaw runner credits; use
# explicit on-demand capacity instead of low-priority spot, whose regional
# quota is too small for broad maintainer proof or parallel Crabbox lanes.
# Fail closed instead of silently falling back to on-demand while the
# Azure-backed billing account is the default runner path.
fallback: spot-only
hints: true
actions:
workflow: .github/workflows/crabbox-hydrate.yml
@@ -49,8 +49,8 @@ aws:
region: eu-west-1
rootGB: 400
azure:
# The OpenClaw Azure subscription is reliable in eastus2; eastus rejects the
# same SKUs and can stall provisioning.
# The OpenClaw Azure subscription has reliable D2 spot capacity in eastus2;
# eastus rejects the same SKUs and can stall provisioning.
location: eastus2
sync:
delete: true
@@ -71,16 +71,14 @@ env:
- OPENCLAW_*
ssh:
user: crabbox
# Azure coordinator leases expose SSH on 22. The run wrapper can fall back
# from 2222, but `crabbox job run` hydrates via the configured port directly.
port: "22"
port: "2222"
jobs:
prewarm:
provider: azure
target: linux
class: standard
type: Standard_D4ads_v6
market: on-demand
type: Standard_D2ads_v6
market: spot
idleTimeout: 90m
hydrate:
actions: true
@@ -97,8 +95,8 @@ jobs:
provider: azure
target: linux
class: standard
type: Standard_D4ads_v6
market: on-demand
type: Standard_D2ads_v6
market: spot
idleTimeout: 90m
hydrate:
actions: true
@@ -107,18 +105,7 @@ jobs:
workflow: .github/workflows/crabbox-hydrate.yml
job: hydrate
ref: main
shell: true
command: |
set -euo pipefail
if ! git status --short >/dev/null 2>&1; then
rm -rf .git
git init -q
git add -A
if ! git diff --cached --quiet; then
git -c user.name=OpenClaw -c user.email=ci@openclaw.local commit -q --no-gpg-sign -m remote-check-tree
fi
fi
env CI=1 corepack pnpm check --timed
command: env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 CI=1 corepack pnpm check:changed
stop: always
testbox-changed:
provider: blacksmith-testbox

View File

@@ -139,139 +139,3 @@ jobs:
if: success()
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
check-arm:
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
permissions:
contents: read
name: "check-arm"
runs-on: blacksmith-16vcpu-ubuntu-2404-arm
timeout-minutes: 120
steps:
- name: Begin Testbox
uses: useblacksmith/begin-testbox@d0e04585c26905fdd92c94a09c159544c7ee1b67
with:
testbox_id: ${{ inputs.testbox_id }}
- name: Verify ARM runner
shell: bash
run: |
set -euo pipefail
runner_arch="$(uname -m)"
echo "check-arm runner architecture: ${runner_arch}"
case "$runner_arch" in
aarch64 | arm64)
;;
*)
echo "check-arm requires an ARM64 runner; got ${runner_arch}" >&2
exit 1
;;
esac
- name: Checkout
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
if [[ -z "$CHECKOUT_TOKEN" ]]; then
echo "checkout token is missing" >&2
exit 1
fi
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
}
checkout_attempt() {
local attempt="$1"
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
echo "checkout attempt ${attempt}/5 succeeded"
}
for attempt in 1 2 3 4 5; do
if checkout_attempt "$attempt"; then
exit 0
fi
echo "checkout attempt ${attempt}/5 failed"
sleep $((attempt * 5))
done
echo "checkout failed after 5 attempts" >&2
exit 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
- name: Prepare Testbox shell
shell: bash
run: |
set -euo pipefail
timeout --signal=TERM --kill-after=10s 30s git \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=50 origin \
"+refs/heads/main:refs/remotes/origin/main"
node_bin="$(dirname "$(node -p 'process.execPath')")"
sudo ln -sf "$node_bin/node" /usr/local/bin/node
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
sudo tee /usr/local/bin/pnpm >/dev/null <<'PNPM'
#!/usr/bin/env bash
exec /usr/local/bin/corepack pnpm "$@"
PNPM
sudo chmod 0755 /usr/local/bin/pnpm
- name: Hydrate Testbox provider env helper
shell: bash
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
run: bash scripts/ci-hydrate-testbox-env.sh
- name: Run Testbox
uses: useblacksmith/run-testbox@5ca05834db1d3813554d1dd109e5f2087a8d7cbc
if: success()
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

View File

@@ -22,7 +22,6 @@
"eslint/no-object-constructor": "error",
"eslint/no-param-reassign": "error",
"eslint/no-proto": "error",
"eslint/no-promise-executor-return": "error",
"eslint/no-regex-spaces": "error",
"eslint/no-return-assign": "error",
"eslint/no-sequences": "error",
@@ -36,7 +35,6 @@
"eslint/no-useless-constructor": "error",
"eslint/no-useless-rename": "error",
"eslint/no-useless-return": "error",
"eslint/no-useless-assignment": "error",
"eslint/no-unused-vars": "error",
"eslint/no-warning-comments": "error",
"eslint/no-unmodified-loop-condition": "error",

View File

@@ -57,7 +57,7 @@ Docs: https://docs.openclaw.ai
- Docs/CI: run Mintlify anchor checks through the repo pnpm runner so docs link validation works when pnpm is only available through the hydrated package-manager shim.
- Agents: keep configured fallback model metadata typed so provider params, context-token caps, and media input limits do not break changed-gate typechecks.
- Agents: accept hidden `sessions_send` body aliases before validation while keeping the model-facing `message` schema canonical. (#88229) Thanks @zhangguiping-xydt.
- CI/Crabbox: keep default runner capacity on the Azure credit-backed on-demand D4 lane with the Azure SSH port and a Git-independent full check job, so broad validation avoids low-priority spot quota stalls, hydrate port mismatches, non-Git hydrated workspaces, and stale AWS region hints.
- CI/Crabbox: keep default runner capacity spot-only and provider-neutral so OpenClaw remote validation does not silently fall back to on-demand leases or stale AWS region hints.
- CI/Crabbox: route Crabbox wrapper and Testbox workflow edits to their regression tests so changed-test gates do not silently run zero specs.
- CI/workflows: route workflow sanity helper edits to their guard tests and cover composite-action input interpolation checks.
- CI/tooling: route CI scope, dependency, changelog, and docs helper edits to their owner tests instead of silently skipping changed-test coverage.

View File

@@ -5528,7 +5528,6 @@ public struct SkillsProposalRecordResult: Codable, Sendable {
public let createdat: String
public let updatedat: String
public let createdby: AnyCodable
public let origin: [String: AnyCodable]?
public let proposedversion: String
public let draftfile: String
public let drafthash: String
@@ -5553,7 +5552,6 @@ public struct SkillsProposalRecordResult: Codable, Sendable {
createdat: String,
updatedat: String,
createdby: AnyCodable,
origin: [String: AnyCodable]?,
proposedversion: String,
draftfile: String,
drafthash: String,
@@ -5577,7 +5575,6 @@ public struct SkillsProposalRecordResult: Codable, Sendable {
self.createdat = createdat
self.updatedat = updatedat
self.createdby = createdby
self.origin = origin
self.proposedversion = proposedversion
self.draftfile = draftfile
self.drafthash = drafthash
@@ -5603,7 +5600,6 @@ public struct SkillsProposalRecordResult: Codable, Sendable {
case createdat = "createdAt"
case updatedat = "updatedAt"
case createdby = "createdBy"
case origin
case proposedversion = "proposedVersion"
case draftfile = "draftFile"
case drafthash = "draftHash"

View File

@@ -248,7 +248,7 @@ iMessage catchup is now available as an opt-in feature on the bundled plugin. On
There is no supported BlueBubbles runtime to switch back to. If iMessage verification fails, set `channels.imessage.enabled: false`, restart the Gateway, fix the `imsg` blocker, and retry the cutover.
The reply cache lives in SQLite plugin state. `openclaw doctor --fix` imports and archives the old `imessage/reply-cache.jsonl` sidecar when present.
The reply cache lives at `~/.openclaw/state/imessage/reply-cache.jsonl` (mode `0600`, parent dir `0700`). It is safe to delete if you want a clean slate.
## Related

View File

@@ -533,7 +533,7 @@ When `imsg launch` is running and `openclaw channels status --probe` reports `pr
</Accordion>
<Accordion title="Message IDs">
Inbound iMessage context includes both short `MessageSid` values and full message GUIDs when available. Short IDs are scoped to the recent SQLite-backed reply cache and are checked against the current chat before use. If a short ID has expired or belongs to another chat, retry with the full `MessageSidFull`.
Inbound iMessage context includes both short `MessageSid` values and full message GUIDs when available. Short IDs are scoped to the recent in-memory reply cache and are checked against the current chat before use. If a short ID has expired or belongs to another chat, retry with the full `MessageSidFull`.
</Accordion>
@@ -714,7 +714,7 @@ Each replayed row is fed through the live dispatch path (`evaluateIMessageInboun
### Cursor and retry semantics
Catchup keeps a per-account cursor in SQLite plugin state:
Catchup keeps a per-account cursor at `<openclawStateDir>/imessage/catchup/<account>__<hash>.json` (the OpenClaw state dir defaults to `~/.openclaw`, overridable with `OPENCLAW_STATE_DIR`):
```json
{
@@ -729,7 +729,6 @@ Catchup keeps a per-account cursor in SQLite plugin state:
- After the startup catchup query succeeds, later live-handled rows also advance the same cursor so a gateway restart does not replay messages that were already handled live. Live cursor writes do not jump past catchup failures that are still below `maxFailureRetries`.
- After `maxFailureRetries` consecutive throws against the same `guid`, catchup logs a `warn` and force-advances the cursor past the wedged message so subsequent startups can make progress.
- Already-given-up guids are skipped on sight (no dispatch attempt) on later runs and counted under `skippedGivenUp` in the run summary.
- `openclaw doctor --fix` imports legacy `<openclawStateDir>/imessage/catchup/*.json` cursor files into SQLite plugin state and archives the old files.
### Operator-visible signals

View File

@@ -91,7 +91,7 @@ For the bundled non-ACP Codex harness, OpenClaw applies the same lifecycle by pr
OpenClaw calls two optional subagent lifecycle hooks:
<ParamField path="prepareSubagentSpawn" type="method">
Prepare shared context state before a child run starts. The hook receives parent/child session keys, `contextMode` (`isolated` or `fork`), available transcript ids/files, and optional TTL. If it returns a rollback handle, OpenClaw calls it when spawn fails after preparation succeeds. Native subagent spawns that request `lightContext` and resolve to `contextMode="isolated"` intentionally skip this hook so the child starts from the lightweight bootstrap context without context-engine-managed pre-spawn state.
Prepare shared context state before a child run starts. The hook receives parent/child session keys, `contextMode` (`isolated` or `fork`), available transcript ids/files, and optional TTL. If it returns a rollback handle, OpenClaw calls it when spawn fails after preparation succeeds.
</ParamField>
<ParamField path="onSubagentEnded" type="method">
Clean up when a subagent session completes or is swept.

View File

@@ -76,7 +76,8 @@ Notes:
extra approval scopes:
- commandless request: `operator.pairing`
- non-exec command request: `operator.pairing` + `operator.write`
- `system.run` / `system.run.prepare` / `system.which` request:
- `system.run` / `system.run.prepare` / `system.which` /
`system.execApprovals.*` request:
`operator.pairing` + `operator.admin`
<Warning>

View File

@@ -417,6 +417,13 @@ Nodes must advertise `system.execApprovals.get/set` (macOS app or
headless node host). If a node does not advertise exec approvals yet,
edit its local `~/.openclaw/exec-approvals.json` directly.
Some node hosts, including native Windows hosts, expose host-native
approval snapshots instead of a file-backed OpenClaw approvals file. The
Control UI shows those snapshots as read-only because the native host owns
the policy format and editor. Use the Windows companion app or
`openclaw approvals set --node <id|name|ip>` for supported updates on
those nodes.
CLI: `openclaw approvals` supports gateway or node editing - see
[Approvals CLI](/cli/approvals).

View File

@@ -262,12 +262,7 @@ async function terminatePids(
deps: AcpxProcessCleanupDeps | undefined,
): Promise<number[]> {
const killProcess = deps?.killProcess ?? ((pid, signal) => process.kill(pid, signal));
const sleep =
deps?.sleep ??
((ms) =>
new Promise<void>((resolve) => {
setTimeout(resolve, ms);
}));
const sleep = deps?.sleep ?? ((ms) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
const terminated: number[] = [];
for (const pid of pids) {
@@ -307,7 +302,7 @@ export async function cleanupOpenClawOwnedAcpxProcessTree(params: {
return { inspectedPids: [], terminatedPids: [], skippedReason: "missing-root" };
}
let processes: AcpxProcessInfo[];
let processes: AcpxProcessInfo[] = [];
try {
processes = await (params.deps?.listProcesses ?? listPlatformProcesses)();
} catch {

View File

@@ -1196,7 +1196,7 @@ export class AcpxRuntime implements AcpRuntime {
const record = await this.sessionStore.load(
input.handle.acpxRecordId ?? input.handle.sessionKey,
);
let closeSucceeded;
let closeSucceeded = false;
try {
await this.resolveDelegateForLoadedRecord(input.handle, record).close({
handle: input.handle,

View File

@@ -2958,9 +2958,7 @@ describe("active-memory plugin", () => {
};
plugin.register(api as unknown as OpenClawPluginApi);
runEmbeddedAgent.mockImplementationOnce(async (params: { timeoutMs?: number }) => {
await new Promise((resolve) => {
setTimeout(resolve, (params.timeoutMs ?? 0) + 5);
});
await new Promise((resolve) => setTimeout(resolve, (params.timeoutMs ?? 0) + 5));
return {
payloads: [{ text: "late timeout payload that should never become memory context" }],
meta: { aborted: true },
@@ -3003,9 +3001,7 @@ describe("active-memory plugin", () => {
};
plugin.register(api as unknown as OpenClawPluginApi);
runEmbeddedAgent.mockImplementationOnce(async () => {
await new Promise((resolve) => {
setTimeout(resolve, CONFIGURED_TIMEOUT_MS + 5);
});
await new Promise((resolve) => setTimeout(resolve, CONFIGURED_TIMEOUT_MS + 5));
return { payloads: [{ text: "remember the ramen place" }] };
});
@@ -3135,9 +3131,7 @@ describe("active-memory plugin", () => {
},
},
]);
await new Promise((resolve) => {
setTimeout(resolve, 35);
});
await new Promise((resolve) => setTimeout(resolve, 35));
return { payloads: [{ text: "User usually orders ramen." }] };
});
@@ -3227,9 +3221,7 @@ describe("active-memory plugin", () => {
},
},
]);
await new Promise((resolve) => {
setTimeout(resolve, 35);
});
await new Promise((resolve) => setTimeout(resolve, 35));
return { payloads: [{ text: "User usually orders ramen after late flights." }] };
});

View File

@@ -42,9 +42,7 @@ import { BrowserCdpEndpointBlockedError } from "./errors.js";
async function startWsServer() {
const wss = new WebSocketServer({ port: 0, host: "127.0.0.1" });
await new Promise<void>((resolve) => {
wss.once("listening", () => resolve());
});
await new Promise<void>((resolve) => wss.once("listening", () => resolve()));
const port = (wss.address() as { port: number }).port;
return { wss, port, url: `ws://127.0.0.1:${port}/devtools/browser/TEST` };
}
@@ -57,9 +55,7 @@ describe("cdp.helpers internal", () => {
registerManagedProxyBrowserCdpBypassMock.mockReset();
registerManagedProxyBrowserCdpBypassMock.mockImplementation(() => undefined);
if (wss) {
await new Promise<void>((resolve) => {
wss?.close(() => resolve());
});
await new Promise<void>((resolve) => wss?.close(() => resolve()));
wss = null;
}
});
@@ -311,9 +307,7 @@ describe("cdp.helpers internal", () => {
cb(true);
},
});
await new Promise<void>((resolve) => {
wss?.once("listening", () => resolve());
});
await new Promise<void>((resolve) => wss?.once("listening", () => resolve()));
const port = (wss.address() as { port: number }).port;
let callbackCount = 0;
wss.on("connection", (socket) => {
@@ -347,9 +341,7 @@ describe("cdp.helpers internal", () => {
cb(false, 429, "too many requests");
},
});
await new Promise<void>((resolve) => {
wss?.once("listening", () => resolve());
});
await new Promise<void>((resolve) => wss?.once("listening", () => resolve()));
const port = (wss.address() as { port: number }).port;
await expect(

View File

@@ -397,9 +397,7 @@ type CdpSocketOptions = {
};
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
return new Promise((resolve) => setTimeout(resolve, ms));
}
function normalizeRetryCount(value: number | undefined, fallback: number): number {

View File

@@ -79,9 +79,7 @@ function replyToViewportCommandOrScreenshot(
async function startMockWsServer(handle: CdpReplyHandler) {
const wss = new WebSocketServer({ port: 0, host: "127.0.0.1" });
await new Promise<void>((resolve) => {
wss.once("listening", () => resolve());
});
await new Promise<void>((resolve) => wss.once("listening", () => resolve()));
const port = (wss.address() as { port: number }).port;
wss.on("connection", (socket) => {
socket.on("message", (raw) => {
@@ -115,9 +113,7 @@ describe("cdp internal", () => {
afterEach(async () => {
if (wss) {
await new Promise<void>((resolve) => {
wss?.close(() => resolve());
});
await new Promise<void>((resolve) => wss?.close(() => resolve()));
wss = null;
}
});
@@ -1076,9 +1072,7 @@ describe("cdp internal", () => {
// in createTargetViaCdp — the bare-ws root triggers discovery.
const http = await import("node:http");
const wsServer = new WebSocketServer({ port: 0, host: "127.0.0.1" });
await new Promise<void>((resolve) => {
wsServer.once("listening", () => resolve());
});
await new Promise<void>((resolve) => wsServer.once("listening", () => resolve()));
const wsPort = (wsServer.address() as { port: number }).port;
wsServer.on("connection", (socket) => {
socket.on("message", (raw) => {
@@ -1116,9 +1110,7 @@ describe("cdp internal", () => {
}
res.writeHead(404).end();
});
await new Promise<void>((resolve) => {
httpServer.listen(0, "127.0.0.1", () => resolve());
});
await new Promise<void>((resolve) => httpServer.listen(0, "127.0.0.1", () => resolve()));
const httpPort = (httpServer.address() as { port: number }).port;
try {
const out = await createTargetViaCdp({
@@ -1127,12 +1119,8 @@ describe("cdp internal", () => {
});
expect(out.targetId).toBe("T_BARE_WS");
} finally {
await new Promise<void>((resolve) => {
wsServer.close(() => resolve());
});
await new Promise<void>((resolve) => {
httpServer.close(() => resolve());
});
await new Promise<void>((resolve) => wsServer.close(() => resolve()));
await new Promise<void>((resolve) => httpServer.close(() => resolve()));
}
});

View File

@@ -27,9 +27,7 @@ describe("cdp", () => {
const startWsServer = async () => {
wsServer = new WebSocketServer({ port: 0, host: "127.0.0.1" });
await new Promise<void>((resolve) => {
wsServer?.once("listening", resolve);
});
await new Promise<void>((resolve) => wsServer?.once("listening", resolve));
return (wsServer.address() as { port: number }).port;
};
@@ -79,9 +77,7 @@ describe("cdp", () => {
res.statusCode = 404;
res.end("not found");
});
await new Promise<void>((resolve) => {
httpServer?.listen(0, "127.0.0.1", resolve);
});
await new Promise<void>((resolve) => httpServer?.listen(0, "127.0.0.1", resolve));
return (httpServer.address() as { port: number }).port;
};
@@ -89,16 +85,14 @@ describe("cdp", () => {
vi.unstubAllEnvs();
await new Promise<void>((resolve) => {
if (!httpServer) {
resolve();
return;
return resolve();
}
httpServer.close(() => resolve());
httpServer = null;
});
await new Promise<void>((resolve) => {
if (!wsServer) {
resolve();
return;
return resolve();
}
wsServer.close(() => resolve());
wsServer = null;
@@ -196,9 +190,7 @@ describe("cdp", () => {
res.statusCode = 404;
res.end("not found");
});
await new Promise<void>((resolve) => {
httpServer?.listen(0, "127.0.0.1", resolve);
});
await new Promise<void>((resolve) => httpServer?.listen(0, "127.0.0.1", resolve));
const httpPort = (httpServer.address() as AddressInfo).port;
await expect(
@@ -218,9 +210,7 @@ describe("cdp", () => {
heldSockets.push(socket);
// Hold the TCP connection open without completing the WebSocket handshake.
});
await new Promise<void>((resolve) => {
httpServer?.listen(0, "127.0.0.1", resolve);
});
await new Promise<void>((resolve) => httpServer?.listen(0, "127.0.0.1", resolve));
const port = (httpServer.address() as AddressInfo).port;
try {
@@ -517,9 +507,7 @@ describe("cdp", () => {
}
});
});
await new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", resolve);
});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const addr = server.address() as AddressInfo;
const created = await createTargetViaCdp({
@@ -528,12 +516,8 @@ describe("cdp", () => {
});
expect(created.targetId).toBe("ROOT_FALLBACK");
} finally {
await new Promise<void>((resolve) => {
wss.close(() => resolve());
});
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
await new Promise<void>((resolve) => wss.close(() => resolve()));
await new Promise<void>((resolve) => server.close(() => resolve()));
}
});

View File

@@ -137,7 +137,7 @@ async function diagnoseCdpHealthCommand(
if (settled) {
return;
}
let parsed: { id?: unknown; result?: unknown } | null;
let parsed: { id?: unknown; result?: unknown } | null = null;
try {
parsed = JSON.parse(rawDataToString(raw)) as { id?: unknown; result?: unknown };
} catch {

View File

@@ -194,12 +194,8 @@ async function withMockChromeCdpServer(params: {
const addr = server.address() as AddressInfo;
await params.run(`http://127.0.0.1:${addr.port}`);
} finally {
await new Promise<void>((resolve) => {
wss.close(() => resolve());
});
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
await new Promise<void>((resolve) => wss.close(() => resolve()));
await new Promise<void>((resolve) => server.close(() => resolve()));
}
}
@@ -956,13 +952,9 @@ describe("chrome.ts internal", () => {
it("resolves false when the direct-ws probe cannot connect", async () => {
// Bind a ws server and then close it, so connecting to it fails.
const wss = new WebSocketServer({ port: 0, host: "127.0.0.1" });
await new Promise<void>((resolve) => {
wss.once("listening", () => resolve());
});
await new Promise<void>((resolve) => wss.once("listening", () => resolve()));
const port = (wss.address() as { port: number }).port;
await new Promise<void>((resolve) => {
wss.close(() => resolve());
});
await new Promise<void>((resolve) => wss.close(() => resolve()));
await expect(
isChromeReachable(`ws://127.0.0.1:${port}/devtools/browser/GONE`, 50),
).resolves.toBe(false);
@@ -970,9 +962,7 @@ describe("chrome.ts internal", () => {
it("resolves true when the direct-ws handshake succeeds", async () => {
const wss = new WebSocketServer({ port: 0, host: "127.0.0.1" });
await new Promise<void>((resolve) => {
wss.once("listening", () => resolve());
});
await new Promise<void>((resolve) => wss.once("listening", () => resolve()));
const port = (wss.address() as { port: number }).port;
try {
// Direct /devtools/ WS URL — isChromeReachable goes through
@@ -982,9 +972,7 @@ describe("chrome.ts internal", () => {
isChromeReachable(`ws://127.0.0.1:${port}/devtools/browser/OK`, 500),
).resolves.toBe(true);
} finally {
await new Promise<void>((resolve) => {
wss.close(() => resolve());
});
await new Promise<void>((resolve) => wss.close(() => resolve()));
}
});
});
@@ -1006,13 +994,9 @@ describe("chrome.ts internal", () => {
// accepting ws upgrades — the canRunCdpHealthCommand probe will
// fire its 'error' handler during handshake.
const dead = new WebSocketServer({ port: 0, host: "127.0.0.1" });
await new Promise<void>((resolve) => {
dead.once("listening", () => resolve());
});
await new Promise<void>((resolve) => dead.once("listening", () => resolve()));
const deadPort = (dead.address() as { port: number }).port;
await new Promise<void>((resolve) => {
dead.close(() => resolve());
});
await new Promise<void>((resolve) => dead.close(() => resolve()));
const server = createServer((req, res) => {
if (req.url === "/json/version") {
res.writeHead(200, { "Content-Type": "application/json" });
@@ -1025,18 +1009,14 @@ describe("chrome.ts internal", () => {
}
res.writeHead(404).end();
});
await new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", () => resolve());
});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
try {
const addr = server.address() as AddressInfo;
await expect(isChromeCdpReady(`http://127.0.0.1:${addr.port}`, 50, 10)).resolves.toBe(
false,
);
} finally {
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
await new Promise<void>((resolve) => server.close(() => resolve()));
}
});

View File

@@ -42,12 +42,14 @@ async function startLoopbackCdpServer(): Promise<RunningServer> {
afterEach(async () => {
await Promise.all(
runningServers.splice(0).map(
(server) =>
new Promise<void>((resolve, reject) => {
server.close((err) => (err ? reject(err) : resolve()));
}),
),
runningServers
.splice(0)
.map(
(server) =>
new Promise<void>((resolve, reject) =>
server.close((err) => (err ? reject(err) : resolve())),
),
),
);
});

View File

@@ -108,12 +108,8 @@ async function withMockChromeCdpServer(params: {
const addr = server.address() as AddressInfo;
await params.run(`http://127.0.0.1:${addr.port}`);
} finally {
await new Promise<void>((resolve) => {
wss.close(() => resolve());
});
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
await new Promise<void>((resolve) => wss.close(() => resolve()));
await new Promise<void>((resolve) => server.close(() => resolve()));
}
}
@@ -553,9 +549,7 @@ describe("browser chrome helpers", () => {
}),
).rejects.toBeInstanceOf(BrowserCdpEndpointBlockedError);
} finally {
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
await new Promise<void>((resolve) => server.close(() => resolve()));
}
});
@@ -565,7 +559,7 @@ describe("browser chrome helpers", () => {
onConnection: (wss) => {
wss.on("connection", (ws) => {
ws.on("message", (raw) => {
let message: { id?: unknown; method?: unknown } | null;
let message: { id?: unknown; method?: unknown } | null = null;
try {
const text =
typeof raw === "string"
@@ -761,12 +755,8 @@ describe("browser chrome helpers", () => {
expect(diagnostic.wsUrl).toBe(wsOnlyBase);
expect(diagnostic.browser).toBe("Browserless/Mock");
} finally {
await new Promise<void>((resolve) => {
wss.close(() => resolve());
});
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
await new Promise<void>((resolve) => wss.close(() => resolve()));
await new Promise<void>((resolve) => server.close(() => resolve()));
}
});
@@ -795,16 +785,12 @@ describe("browser chrome helpers", () => {
);
// A real WS server accepts the handshake.
const wss = new WebSocketServer({ port: 0, host: "127.0.0.1" });
await new Promise<void>((resolve) => {
wss.once("listening", () => resolve());
});
await new Promise<void>((resolve) => wss.once("listening", () => resolve()));
const port = (wss.address() as AddressInfo).port;
try {
await expect(isChromeReachable(`ws://127.0.0.1:${port}`, 500)).resolves.toBe(true);
} finally {
await new Promise<void>((resolve) => {
wss.close(() => resolve());
});
await new Promise<void>((resolve) => wss.close(() => resolve()));
}
});
@@ -825,9 +811,7 @@ describe("browser chrome helpers", () => {
}
});
});
await new Promise<void>((resolve) => {
wss.once("listening", () => resolve());
});
await new Promise<void>((resolve) => wss.once("listening", () => resolve()));
const port = (wss.address() as AddressInfo).port;
try {
await expect(isChromeCdpReady(`ws://127.0.0.1:${port}`, 500, 500)).resolves.toBe(true);
@@ -836,9 +820,7 @@ describe("browser chrome helpers", () => {
);
expect(diagnostic.wsUrl).toBe(`ws://127.0.0.1:${port}`);
} finally {
await new Promise<void>((resolve) => {
wss.close(() => resolve());
});
await new Promise<void>((resolve) => wss.close(() => resolve()));
}
});

View File

@@ -519,9 +519,7 @@ export async function launchOpenClawChrome(
if (exists(localStatePath) && exists(preferencesPath)) {
break;
}
await new Promise((r) => {
setTimeout(r, CHROME_BOOTSTRAP_PREFS_POLL_MS);
});
await new Promise((r) => setTimeout(r, CHROME_BOOTSTRAP_PREFS_POLL_MS));
}
try {
bootstrap.kill("SIGTERM");
@@ -533,9 +531,7 @@ export async function launchOpenClawChrome(
if (bootstrap.exitCode != null) {
break;
}
await new Promise((r) => {
setTimeout(r, CHROME_BOOTSTRAP_EXIT_POLL_MS);
});
await new Promise((r) => setTimeout(r, CHROME_BOOTSTRAP_EXIT_POLL_MS));
}
}
@@ -581,9 +577,7 @@ export async function launchOpenClawChrome(
launchHttpReachable = true;
break;
}
await new Promise((r) => {
setTimeout(r, CHROME_LAUNCH_READY_POLL_MS);
});
await new Promise((r) => setTimeout(r, CHROME_LAUNCH_READY_POLL_MS));
}
if (!launchHttpReachable) {
@@ -688,9 +682,7 @@ export async function stopOpenClawChrome(
return;
}
const remainingMs = timeoutMs - (Date.now() - start);
await new Promise((r) => {
setTimeout(r, Math.max(1, Math.min(100, remainingMs)));
});
await new Promise((r) => setTimeout(r, Math.max(1, Math.min(100, remainingMs))));
}
try {

View File

@@ -37,9 +37,7 @@ describe("browser client fetch attachOnly diagnostics", () => {
socket.on("close", () => sockets.delete(socket));
socket.on("error", () => {});
});
await new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", resolve);
});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const port = (server.address() as { port: number }).port;
const configPath = path.join(tempHome.home, ".openclaw", "openclaw.json");
await fs.writeFile(
@@ -80,9 +78,7 @@ describe("browser client fetch attachOnly diagnostics", () => {
for (const socket of sockets) {
socket.destroy();
}
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
await new Promise<void>((resolve) => server.close(() => resolve()));
}
});
});

View File

@@ -469,7 +469,7 @@ export function resolveProfile(
const rawProfileUrl = profile.cdpUrl?.trim() ?? "";
let cdpHost = resolved.cdpHost;
let cdpPort = profile.cdpPort ?? 0;
let cdpUrl;
let cdpUrl = "";
const driver = profile.driver === "existing-session" ? "existing-session" : "openclaw";
const headless = profile.headless ?? resolved.headless;
const headlessSource =

View File

@@ -212,9 +212,7 @@ describe("pw-session ensurePageState", () => {
try {
handlers.get("download")?.[0]?.(download);
await new Promise((resolve) => {
setImmediate(resolve);
});
await new Promise((resolve) => setImmediate(resolve));
expect(unhandled).toStrictEqual([]);
await expect(download.path?.()).rejects.toThrow("save failed");

View File

@@ -947,9 +947,7 @@ async function connectBrowser(cdpUrl: string, ssrfPolicy?: SsrFPolicy): Promise<
break;
}
const delay = resolveCdpConnectRetryDelayMs(attempt);
await new Promise((r) => {
setTimeout(r, delay);
});
await new Promise((r) => setTimeout(r, delay));
}
}
if (lastErr instanceof Error) {
@@ -1068,7 +1066,7 @@ async function findPageByTargetId(
const pages = await getAllPages(browser);
let resolvedViaCdp = false;
for (const page of pages) {
let tid: string | null;
let tid: string | null = null;
try {
tid = await pageTargetId(page);
resolvedViaCdp = true;
@@ -1172,7 +1170,7 @@ export async function getPageForTargetId(opts: {
}
function isTopLevelNavigationRequest(page: Page, request: Request): boolean {
let sameMainFrame;
let sameMainFrame = false;
try {
sameMainFrame = request.frame() === page.mainFrame();
} catch {
@@ -1199,7 +1197,7 @@ function isTopLevelNavigationRequest(page: Page, request: Request): boolean {
}
function isSubframeDocumentNavigationRequest(page: Page, request: Request): boolean {
let sameMainFrame;
let sameMainFrame = false;
try {
sameMainFrame = request.frame() === page.mainFrame();
} catch {

View File

@@ -581,9 +581,7 @@ export async function clickViaPlaywright(opts: {
abortPromise,
reconcileRemoteDialog,
);
await new Promise((resolve) => {
setTimeout(resolve, delayMs);
});
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
if (opts.doubleClick) {
await awaitActionWithAbort(

View File

@@ -45,9 +45,7 @@ import type { BrowserRouteRegistrar } from "./types.js";
import { asyncBrowserRoute, jsonError, toStringOrEmpty } from "./utils.js";
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
return new Promise((resolve) => setTimeout(resolve, ms));
}
const EXISTING_SESSION_INTERACTION_NAVIGATION_RECHECK_DELAYS_MS = [0, 250, 500] as const;

View File

@@ -34,9 +34,7 @@ export async function resolveTargetIdAfterNavigate(opts: {
const first = pickReplacement(await opts.listTabs());
currentTargetId = first.targetId;
if (first.shouldRetry) {
await new Promise((r) => {
setTimeout(r, opts.retryDelayMs ?? 800);
});
await new Promise((r) => setTimeout(r, opts.retryDelayMs ?? 800));
currentTargetId = pickReplacement(await opts.listTabs(), {
allowSingleTabFallback: true,
}).targetId;

View File

@@ -286,9 +286,7 @@ export function createProfileAvailability({
if (await isReachable(attemptTimeoutMs)) {
return;
}
await new Promise((r) => {
setTimeout(r, CDP_READY_AFTER_LAUNCH_POLL_MS);
});
await new Promise((r) => setTimeout(r, CDP_READY_AFTER_LAUNCH_POLL_MS));
}
throw new Error(
`Chrome CDP websocket for profile "${profile.name}" is not reachable after start. ${await describeCdpFailure(
@@ -308,9 +306,7 @@ export function createProfileAvailability({
} catch (err) {
lastError = err;
}
await new Promise((r) => {
setTimeout(r, CHROME_MCP_ATTACH_READY_POLL_MS);
});
await new Promise((r) => setTimeout(r, CHROME_MCP_ATTACH_READY_POLL_MS));
}
throw new BrowserProfileUnavailableError(formatChromeMcpAttachFailure(lastError));
};

View File

@@ -350,9 +350,7 @@ export function createProfileTabOps({
triggerManagedTabLimit(found.targetId);
return assignTabAlias({ profileState, tab: found, label: opts?.label });
}
await new Promise((r) => {
setTimeout(r, OPEN_TAB_DISCOVERY_POLL_MS);
});
await new Promise((r) => setTimeout(r, OPEN_TAB_DISCOVERY_POLL_MS));
}
triggerManagedTabLimit(createdViaCdp);
return assignTabAlias({

View File

@@ -21,9 +21,7 @@ function isTransientStartupFetchError(error: unknown): boolean {
}
async function sleep(ms: number): Promise<void> {
await new Promise((resolve) => {
setTimeout(resolve, ms);
});
await new Promise((resolve) => setTimeout(resolve, ms));
}
async function postStartWithRetry(params: {

View File

@@ -41,9 +41,7 @@ describe("browser control HTTP auth", () => {
if (!current) {
return;
}
await new Promise<void>((resolve) => {
current.close(() => resolve());
});
await new Promise<void>((resolve) => current.close(() => resolve()));
});
it("requires bearer auth for standalone browser HTTP routes", async () => {

View File

@@ -150,7 +150,7 @@ function formatDoctorLine(check: BrowserDoctorCheck): string {
async function runBrowserDoctor(parent: BrowserParentOpts, profile?: string, deep?: boolean) {
const checks: BrowserDoctorCheck[] = [];
let status: BrowserStatus | null;
let status: BrowserStatus | null = null;
try {
status = await fetchBrowserStatus(parent, profile);

View File

@@ -171,7 +171,7 @@ export async function handleBrowserGatewayRequest({
}
const cfg = getRuntimeConfig();
let nodeTarget: NodeSession | null;
let nodeTarget: NodeSession | null = null;
try {
nodeTarget = resolveBrowserNodeTarget({
cfg,

View File

@@ -388,6 +388,7 @@ describe("canvas host", () => {
const linkName = `test-link-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`;
const linkPath = path.join(a2uiRoot, linkName);
let createdBundle = false;
let createdLink = false;
try {
await fs.stat(bundlePath);
@@ -397,6 +398,7 @@ describe("canvas host", () => {
}
await fs.symlink(path.join(process.cwd(), "package.json"), linkPath);
createdLink = true;
try {
const res = await captureA2uiResponse(`${A2UI_PATH}/`);
@@ -419,7 +421,9 @@ describe("canvas host", () => {
expect(symlinkRes.status).toBe(404);
expect(symlinkRes.body).toBe("not found");
} finally {
await fs.rm(linkPath, { force: true });
if (createdLink) {
await fs.rm(linkPath, { force: true });
}
if (createdBundle) {
await fs.rm(bundlePath, { force: true });
}

View File

@@ -443,9 +443,7 @@ export async function createCanvasHostHandler(
}
}
if (wss) {
await new Promise<void>((resolve) => {
wss.close(() => resolve());
});
await new Promise<void>((resolve) => wss.close(() => resolve()));
}
},
};
@@ -530,9 +528,9 @@ export async function startCanvasHost(opts: CanvasHostServerOpts): Promise<Canva
if (ownsHandler) {
await handler.close();
}
await new Promise<void>((resolve, reject) => {
server.close((err) => (err ? reject(err) : resolve()));
});
await new Promise<void>((resolve, reject) =>
server.close((err) => (err ? reject(err) : resolve())),
);
},
};
}

View File

@@ -121,9 +121,7 @@ describe("ClickClack gateway", () => {
await vi.waitFor(() => expect(mocks.client.websocket).toHaveBeenCalledTimes(1));
socket.emit("message", Buffer.from("{not json"));
await new Promise((resolve) => {
setImmediate(resolve);
});
await new Promise((resolve) => setImmediate(resolve));
expect(runError).toBeUndefined();
expect(ctx.log?.warn).toHaveBeenCalledWith(
"[default] skipped malformed ClickClack websocket event",

View File

@@ -190,9 +190,7 @@ export async function startClickClackGatewayAccount(
socket.on("error", reject);
});
if (!ctx.abortSignal.aborted) {
await new Promise((resolve) => {
setTimeout(resolve, account.reconnectMs);
});
await new Promise((resolve) => setTimeout(resolve, account.reconnectMs));
}
}
ctx.setStatus({ accountId: account.accountId, running: false });

View File

@@ -790,9 +790,7 @@ async function waitForFile(filePath: string): Promise<string> {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
throw error;
}
await new Promise((resolve) => {
setTimeout(resolve, 20);
});
await new Promise((resolve) => setTimeout(resolve, 20));
}
}
throw new Error(`timed out waiting for ${filePath}`);
@@ -840,14 +838,10 @@ describe("connectCodexAppServerEndpoint", () => {
await expect(
Promise.race([
probe,
new Promise((_, reject) => {
setTimeout(() => reject(new Error("probe timed out")), 500);
}),
new Promise((_, reject) => setTimeout(() => reject(new Error("probe timed out")), 500)),
]),
).resolves.toMatchObject([{ endpointId: "ws", ok: false }]);
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
await new Promise<void>((resolve) => server.close(() => resolve()));
});
it("rejects malformed stdio frames instead of throwing out of band", async () => {
@@ -936,9 +930,7 @@ describe("connectCodexAppServerEndpoint", () => {
);
await expect(supervisor.probeEndpoints()).resolves.toEqual([{ endpointId: "exits", ok: true }]);
await new Promise((resolve) => {
setTimeout(resolve, 50);
});
await new Promise((resolve) => setTimeout(resolve, 50));
await expect(supervisor.probeEndpoints()).resolves.toMatchObject([
{
endpointId: "exits",

View File

@@ -337,6 +337,7 @@ export async function startCodexAttemptThread(params: {
if (startupClientForAbandonedRequestCleanup === failedClient) {
startupClientForAbandonedRequestCleanup = undefined;
}
attemptedClient = undefined;
if (attempt >= CODEX_APP_SERVER_STARTUP_CONNECTION_CLOSE_MAX_ATTEMPTS) {
embeddedAgentLog.warn(
"codex app-server connection closed during startup; retries exhausted",

View File

@@ -147,7 +147,7 @@ describe("Codex app-server attempt timeouts", () => {
}, 5);
});
},
operation: async () => new Promise<never>(() => {}),
operation: async () => new Promise<never>(() => undefined),
});
const rejected = expect(run).rejects.toThrow("codex app-server startup timed out");
@@ -164,7 +164,7 @@ describe("Codex app-server attempt timeouts", () => {
const run = withCodexStartupTimeout({
timeoutMs: 1_000,
signal: controller.signal,
operation: async () => new Promise<never>(() => {}),
operation: async () => new Promise<never>(() => undefined),
});
const rejected = expect(run).rejects.toThrow("codex app-server startup aborted");

View File

@@ -486,7 +486,7 @@ describe("CodexAppServerClient", () => {
clients.push(harness.client);
harness.client.addRequestHandler((request) => {
if (request.method === "item/tool/call") {
return new Promise<never>(() => {});
return new Promise<never>(() => undefined);
}
return undefined;
});

View File

@@ -179,32 +179,6 @@ describe("Codex app-server dynamic tool build", () => {
expect(resolveCodexDynamicToolsLoading({}, privateQaCodexEnv)).toBe("direct");
});
it("quarantines unreadable tool entries before Codex-specific filtering", async () => {
const messageTool = createRuntimeDynamicTool("message");
const sourceTools = new Proxy([messageTool] as RuntimeDynamicToolForTest[], {
get(target, property, receiver) {
if (property === "0") {
throw new Error("fuzzplugin tool entry getter exploded");
}
if (property === "1") {
return messageTool;
}
if (property === "length") {
return 2;
}
return Reflect.get(target, property, receiver);
},
});
setOpenClawCodingToolsFactoryForTests(() => sourceTools);
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
const params = createParams(sessionFile, workspaceDir);
params.disableTools = false;
params.runtimePlan = createCodexRuntimePlanFixture();
await expect(buildDynamicToolsForTest(params, workspaceDir)).resolves.toEqual([messageTool]);
});
it("limits Codex memory flush runs to managed read and write tools", async () => {
const factoryOptions: unknown[] = [];
setOpenClawCodingToolsFactoryForTests((options) => {

View File

@@ -2,7 +2,6 @@ import {
buildAgentHookContextChannelFields,
buildEmbeddedAttemptToolRunContext,
embeddedAgentLog,
filterProviderNormalizableTools,
isSubagentSessionKey,
normalizeAgentRuntimeTools,
resolveAttemptSpawnWorkspaceDir,
@@ -10,7 +9,6 @@ import {
resolveSandboxContext,
supportsModelTools,
type EmbeddedRunAttemptParams,
type RuntimeToolSchemaDiagnostic,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { resolveAgentDir } from "openclaw/plugin-sdk/agent-runtime";
import { isToolAllowed } from "openclaw/plugin-sdk/sandbox";
@@ -267,19 +265,15 @@ export async function buildDynamicTools(input: DynamicToolBuildParams) {
},
});
toolBuildStages.mark("create-openclaw-coding-tools");
const preNormalizationDiagnostics: RuntimeToolSchemaDiagnostic[] = [];
const readableAllToolProjection = filterProviderNormalizableTools(allTools);
preNormalizationDiagnostics.push(...readableAllToolProjection.diagnostics);
const readableAllTools = [...readableAllToolProjection.tools];
const codexFilteredTools = addNodeShellDynamicToolsIfNeeded(
addSandboxShellDynamicToolsIfAvailable(
isCodexMemoryFlushRun(params)
? filterCodexMemoryFlushDynamicTools(readableAllTools)
: filterCodexDynamicTools(readableAllTools, input.pluginConfig),
readableAllTools,
? filterCodexMemoryFlushDynamicTools(allTools)
: filterCodexDynamicTools(allTools, input.pluginConfig),
allTools,
input,
),
readableAllTools,
allTools,
input,
);
toolBuildStages.mark("codex-filtering");
@@ -301,25 +295,8 @@ export async function buildDynamicTools(input: DynamicToolBuildParams) {
modelId: params.modelId,
modelApi: params.model.api,
model: params.model,
onPreNormalizationSchemaDiagnostics: (diagnostics) =>
preNormalizationDiagnostics.push(...diagnostics),
});
toolBuildStages.mark("runtime-normalization");
if (preNormalizationDiagnostics.length > 0) {
embeddedAgentLog.warn(
`codex app-server quarantined ${preNormalizationDiagnostics.length} unsupported runtime tool schema${preNormalizationDiagnostics.length === 1 ? "" : "s"} before dynamic tool registration`,
{
runId: params.runId,
sessionId: params.sessionId,
diagnostics: preNormalizationDiagnostics.map((diagnostic) => ({
index: diagnostic.toolIndex,
tool: diagnostic.toolName,
violations: diagnostic.violations.slice(0, 12),
violationCount: diagnostic.violations.length,
})),
},
);
}
const summary = toolBuildStages.snapshot();
if (shouldWarnCodexDynamicToolBuildStageSummary(summary)) {
const phase = input.forceHeartbeatTool ? "registered-tools" : "runtime-tools";
@@ -331,7 +308,7 @@ export async function buildDynamicTools(input: DynamicToolBuildParams) {
phase,
totalMs: summary.totalMs,
stages: summary.stages,
allToolCount: readableAllTools.length,
allToolCount: allTools.length,
codexFilteredToolCount: codexFilteredTools.length,
visionFilteredToolCount: visionFilteredTools.length,
filteredToolCount: filteredTools.length,

View File

@@ -194,7 +194,7 @@ describe("dynamic tool execution helpers", () => {
toolBridge: {
handleToolCall: vi.fn((_call, options) => {
capturedSignal = options?.signal;
return new Promise<never>(() => {});
return new Promise<never>(() => undefined);
}),
},
signal: new AbortController().signal,
@@ -230,7 +230,7 @@ describe("dynamic tool execution helpers", () => {
arguments: { action: "poll", sessionId: "process-session", timeout: 30_000 },
},
toolBridge: {
handleToolCall: vi.fn(() => new Promise<never>(() => {})),
handleToolCall: vi.fn(() => new Promise<never>(() => undefined)),
},
signal: new AbortController().signal,
timeoutMs: 1,

View File

@@ -35,9 +35,7 @@ const tinyPngBase64 =
type ProjectorNotification = Parameters<CodexAppServerEventProjector["handleNotification"]>[0];
function flushDiagnosticEvents() {
return new Promise<void>((resolve) => {
setImmediate(resolve);
});
return new Promise<void>((resolve) => setImmediate(resolve));
}
function assistantMessage(text: string, timestamp: number) {

View File

@@ -52,30 +52,8 @@ function createRuntime() {
error?: string;
}>;
};
const createRunningTaskRun = vi.fn(
(params): AgentHarnessTaskRecord => ({
taskId: params.sourceId ?? params.runId,
runtime: "subagent",
sourceId: params.sourceId,
requesterSessionKey: "agent:main:main",
ownerKey: "agent:main:main",
scopeKind: "session",
agentId: params.agentId,
runId: params.runId,
label: params.label,
task: params.task,
status: "running",
deliveryStatus: params.deliveryStatus ?? "not_applicable",
notifyPolicy: params.notifyPolicy ?? "silent",
createdAt: params.startedAt ?? Date.now(),
startedAt: params.startedAt,
lastEventAt: params.lastEventAt,
progressSummary: params.progressSummary,
}),
);
const taskRuntime = {
createRunningTaskRun,
tryCreateRunningTaskRun: vi.fn((params) => createRunningTaskRun(params)),
createRunningTaskRun: vi.fn(),
recordTaskRunProgressByRunId: vi.fn(() => []),
finalizeTaskRunByRunId: vi.fn(() => []),
listTaskRecords: vi.fn((): AgentHarnessTaskRecord[] => []),

View File

@@ -7,7 +7,7 @@ import {
function createRuntime() {
return {
tryCreateRunningTaskRun: vi.fn((params) => ({ taskId: "task-native-subagent", ...params })),
createRunningTaskRun: vi.fn(),
recordTaskRunProgressByRunId: vi.fn(() => []),
finalizeTaskRunByRunId: vi.fn(() => []),
} as unknown as TaskLifecycleRuntime;
@@ -49,7 +49,7 @@ describe("CodexNativeSubagentTaskMirror", () => {
},
});
expect(runtime.tryCreateRunningTaskRun).toHaveBeenCalledWith({
expect(runtime.createRunningTaskRun).toHaveBeenCalledWith({
sourceId: "codex-thread:child-thread",
agentId: "main",
runId: "codex-thread:child-thread",
@@ -62,7 +62,7 @@ describe("CodexNativeSubagentTaskMirror", () => {
lastEventAt: 20_000,
progressSummary: "Codex native subagent started.",
});
expect(vi.mocked(runtime.tryCreateRunningTaskRun).mock.calls[0]?.[0]).not.toHaveProperty(
expect(vi.mocked(runtime.createRunningTaskRun).mock.calls[0]?.[0]).not.toHaveProperty(
"childSessionKey",
);
expect(runtime.recordTaskRunProgressByRunId).toHaveBeenCalledWith({
@@ -99,7 +99,7 @@ describe("CodexNativeSubagentTaskMirror", () => {
},
});
expect(runtime.tryCreateRunningTaskRun).not.toHaveBeenCalled();
expect(runtime.createRunningTaskRun).not.toHaveBeenCalled();
expect(runtime.recordTaskRunProgressByRunId).not.toHaveBeenCalled();
expect(runtime.finalizeTaskRunByRunId).not.toHaveBeenCalled();
});
@@ -133,7 +133,7 @@ describe("CodexNativeSubagentTaskMirror", () => {
mirror.handleNotification(notification);
mirror.handleNotification(notification);
expect(runtime.tryCreateRunningTaskRun).toHaveBeenCalledTimes(1);
expect(runtime.createRunningTaskRun).toHaveBeenCalledTimes(1);
});
it("maps Codex thread status changes onto the mirrored task run", () => {
@@ -228,7 +228,7 @@ describe("CodexNativeSubagentTaskMirror", () => {
},
});
expect(runtime.tryCreateRunningTaskRun).toHaveBeenCalledWith({
expect(runtime.createRunningTaskRun).toHaveBeenCalledWith({
sourceId: "codex-thread:child-thread",
runId: "codex-thread:child-thread",
label: "Codex subagent",
@@ -240,7 +240,7 @@ describe("CodexNativeSubagentTaskMirror", () => {
lastEventAt: 40_000,
progressSummary: "Codex native subagent spawned.",
});
expect(vi.mocked(runtime.tryCreateRunningTaskRun).mock.calls[0]?.[0]).not.toHaveProperty(
expect(vi.mocked(runtime.createRunningTaskRun).mock.calls[0]?.[0]).not.toHaveProperty(
"childSessionKey",
);
expect(runtime.recordTaskRunProgressByRunId).toHaveBeenCalledWith({
@@ -282,7 +282,7 @@ describe("CodexNativeSubagentTaskMirror", () => {
},
});
expect(runtime.tryCreateRunningTaskRun).toHaveBeenCalledWith(
expect(runtime.createRunningTaskRun).toHaveBeenCalledWith(
expect.objectContaining({
runId: "codex-thread:child-thread",
task: "inspect one thing",
@@ -319,7 +319,7 @@ describe("CodexNativeSubagentTaskMirror", () => {
},
});
expect(runtime.tryCreateRunningTaskRun).toHaveBeenCalledWith(
expect(runtime.createRunningTaskRun).toHaveBeenCalledWith(
expect.objectContaining({
runId: "codex-thread:child-thread",
task: "inspect one thing",

View File

@@ -15,7 +15,7 @@ import { isJsonObject } from "./protocol.js";
export type TaskLifecycleRuntime = Pick<
AgentHarnessTaskRuntime,
"tryCreateRunningTaskRun" | "recordTaskRunProgressByRunId" | "finalizeTaskRunByRunId"
"createRunningTaskRun" | "recordTaskRunProgressByRunId" | "finalizeTaskRunByRunId"
>;
export type CodexNativeSubagentTaskMirrorParams = {
@@ -27,7 +27,6 @@ export type CodexNativeSubagentTaskMirrorParams = {
export class CodexNativeSubagentTaskMirror {
private readonly mirroredThreadIds = new Set<string>();
private readonly failedMirrorThreadIds = new Set<string>();
private readonly terminalRunIds = new Set<string>();
private readonly now: () => number;
@@ -82,7 +81,7 @@ export class CodexNativeSubagentTaskMirror {
trimOptional(thread.preview) ??
`Codex native subagent${label === "Codex subagent" ? "" : ` ${label}`}`;
const createdAt = secondsToMillis(thread.createdAt) ?? this.now();
const taskRecord = this.runtime.tryCreateRunningTaskRun({
this.runtime.createRunningTaskRun({
sourceId: runId,
agentId: this.params.agentId,
runId,
@@ -95,13 +94,6 @@ export class CodexNativeSubagentTaskMirror {
lastEventAt: this.now(),
progressSummary: "Codex native subagent started.",
});
if (!taskRecord) {
this.mirroredThreadIds.delete(threadId);
this.failedMirrorThreadIds.add(threadId);
return;
}
this.failedMirrorThreadIds.delete(threadId);
this.terminalRunIds.delete(runId);
this.applyStatus(threadId, thread.status);
}
@@ -114,9 +106,6 @@ export class CodexNativeSubagentTaskMirror {
}
private applyStatus(threadId: string, status: CodexThreadStatus | null | undefined): void {
if (!this.mirroredThreadIds.has(threadId) && this.failedMirrorThreadIds.has(threadId)) {
return;
}
const statusType = status?.type;
if (!statusType) {
return;
@@ -230,7 +219,7 @@ export class CodexNativeSubagentTaskMirror {
const prompt = trimOptional(readString(item, "prompt"));
const runId = codexNativeSubagentRunId(normalizedThreadId);
const createdAt = this.now();
const taskRecord = this.runtime.tryCreateRunningTaskRun({
this.runtime.createRunningTaskRun({
sourceId: runId,
agentId: this.params.agentId,
runId,
@@ -243,13 +232,6 @@ export class CodexNativeSubagentTaskMirror {
lastEventAt: createdAt,
progressSummary: "Codex native subagent spawned.",
});
if (!taskRecord) {
this.mirroredThreadIds.delete(normalizedThreadId);
this.failedMirrorThreadIds.add(normalizedThreadId);
return;
}
this.failedMirrorThreadIds.delete(normalizedThreadId);
this.terminalRunIds.delete(runId);
}
private applyCollabAgentStatus(
@@ -257,9 +239,6 @@ export class CodexNativeSubagentTaskMirror {
status: string | undefined,
message: string | null | undefined,
): void {
if (!this.mirroredThreadIds.has(threadId) && this.failedMirrorThreadIds.has(threadId)) {
return;
}
const normalizedStatus = normalizeAgentStateStatus(status);
if (!normalizedStatus) {
return;

View File

@@ -85,9 +85,7 @@ async function drainActiveAppServerAttemptsForTest(): Promise<void> {
}
await Promise.race([
Promise.allSettled(attempts.map((attempt) => attempt.promise)),
new Promise<void>((resolve) => {
setTimeout(resolve, 5_000);
}),
new Promise<void>((resolve) => setTimeout(resolve, 5_000)),
]);
}

View File

@@ -66,9 +66,7 @@ describe("runCodexAppServerAttempt hooks and model diagnostics", () => {
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
expect(llmInput).toHaveBeenCalled();
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
const [llmInputPayload, llmInputContext] = mockCall(llmInput, "llm_input") as [
{

View File

@@ -817,7 +817,7 @@ describe("runCodexAppServerAttempt", () => {
onTimeout: async () => {
await releaseCodexSandboxExecServerEnvironment(sandbox);
},
operation: async () => new Promise<never>(() => {}),
operation: async () => new Promise<never>(() => undefined),
}),
).rejects.toThrow("codex app-server startup timed out");
@@ -1111,9 +1111,7 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
const result = await run;
@@ -1686,9 +1684,7 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir));
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
await run;
@@ -1729,9 +1725,7 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
await run;
@@ -1768,9 +1762,7 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir));
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
await run;
@@ -1809,9 +1801,7 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
await harness.completeTurn({ threadId: "thread-existing", turnId: "turn-1" });
await run;
@@ -1856,9 +1846,7 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
await harness.completeTurn({ threadId: "thread-existing", turnId: "turn-1" });
await run;
@@ -1903,9 +1891,7 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
await harness.completeTurn({ threadId: "thread-existing", turnId: "turn-1" });
await run;
@@ -2148,9 +2134,7 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
const result = await run;
@@ -2205,9 +2189,7 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir));
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
const result = await run;
@@ -2284,9 +2266,7 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
const result = await run;
@@ -2474,9 +2454,7 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir));
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
const result = await run;
@@ -2505,9 +2483,7 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
await run;
@@ -2550,9 +2526,7 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
await run;
@@ -2592,9 +2566,7 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
const result = await run;
@@ -2637,9 +2609,7 @@ describe("runCodexAppServerAttempt", () => {
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
await run;
@@ -2881,9 +2851,7 @@ describe("runCodexAppServerAttempt", () => {
const result = await run;
expect(result.aborted).toBe(true);
await new Promise((resolve) => {
setImmediate(resolve);
});
await new Promise((resolve) => setImmediate(resolve));
expect(unhandledRejections).toStrictEqual([]);
} finally {
process.off("unhandledRejection", onUnhandledRejection);
@@ -2975,9 +2943,7 @@ describe("runCodexAppServerAttempt", () => {
{ turnTerminalIdleTimeoutMs: 60_000 },
);
await bufferedTerminal;
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
harness.close();
const result = await run;
@@ -3017,9 +2983,7 @@ describe("runCodexAppServerAttempt", () => {
turnTerminalIdleTimeoutMs: 60_000,
});
await harness.waitForMethod("turn/start");
await new Promise((resolve) => {
setTimeout(resolve, 20);
});
await new Promise((resolve) => setTimeout(resolve, 20));
expect(harness.request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false);
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
@@ -3112,9 +3076,7 @@ describe("runCodexAppServerAttempt", () => {
},
},
});
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
expect(resolved).toBe(false);
await harness.notify({
@@ -3158,9 +3120,7 @@ describe("runCodexAppServerAttempt", () => {
},
},
});
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
expect(resolved).toBe(false);
expect(
warn.mock.calls.some(([message]) =>
@@ -3840,7 +3800,7 @@ describe("runCodexAppServerAttempt", () => {
});
it("times out app-server startup before thread setup can hang forever", async () => {
setCodexAppServerClientFactoryForTest(() => new Promise<never>(() => {}));
setCodexAppServerClientFactoryForTest(() => new Promise<never>(() => undefined));
const params = createParams(
path.join(tempDir, "session.jsonl"),
path.join(tempDir, "workspace"),
@@ -3874,9 +3834,7 @@ describe("runCodexAppServerAttempt", () => {
interval: 1,
});
await waitForMethod("turn/start");
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
await completeTurn({ threadId: "thread-1", turnId: "turn-1" });
await run;
@@ -4349,7 +4307,7 @@ describe("runCodexAppServerAttempt", () => {
const c = {
request: vi.fn(async (method: string) => {
if (method === "thread/start") {
return await new Promise<never>(() => {});
return await new Promise<never>(() => undefined);
}
return {};
}),
@@ -4544,9 +4502,7 @@ describe("runCodexAppServerAttempt", () => {
interval: 1,
});
await waitForMethod("turn/start");
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
await completeTurn({ threadId: "thread-existing", turnId: "turn-1" });
await run;

View File

@@ -965,19 +965,8 @@ export async function runCodexAppServerAttempt(
let client: CodexAppServerClient;
let thread: CodexAppServerThreadLifecycleBinding;
let trajectoryEndRecorded = false;
const markTrajectoryEndRecorded = () => {
trajectoryEndRecorded = true;
};
let nativeHookRelay: NativeHookRelayRegistrationHandle | undefined;
let releaseSharedClientLease: (() => void) | undefined;
const releaseSharedClientLeaseOnce = () => {
const release = releaseSharedClientLease;
if (!release) {
return;
}
releaseSharedClientLease = undefined;
release();
};
let sandboxExecEnvironmentAcquired = false;
const releaseSandboxExecEnvironment = async () => {
if (sandboxExecEnvironmentAcquired) {
@@ -1925,7 +1914,7 @@ export async function runCodexAppServerAttempt(
aborted: runAbortController.signal.aborted,
promptError: turnStartErrorMessage,
});
markTrajectoryEndRecorded();
trajectoryEndRecorded = true;
runAgentHarnessLlmOutputHook({
event: {
runId: params.runId,
@@ -1990,7 +1979,8 @@ export async function runCodexAppServerAttempt(
},
});
params.abortSignal?.removeEventListener("abort", abortFromUpstream);
releaseSharedClientLeaseOnce();
releaseSharedClientLease?.();
releaseSharedClientLease = undefined;
if (usageLimitError) {
await markCodexAuthProfileBlockedFromRateLimits({
params,
@@ -2010,7 +2000,8 @@ export async function runCodexAppServerAttempt(
}
}
if (!turn) {
releaseSharedClientLeaseOnce();
releaseSharedClientLease?.();
releaseSharedClientLease = undefined;
throw new Error("codex app-server turn/start failed without an error");
}
turnIdRef.current = turn.turn.id;
@@ -2259,7 +2250,7 @@ export async function runCodexAppServerAttempt(
yieldDetected,
promptError: normalizeCodexTrajectoryError(finalPromptError),
});
markTrajectoryEndRecorded();
trajectoryEndRecorded = true;
await mirrorTranscriptBestEffort({
params,
agentId: sessionAgentId,
@@ -2436,7 +2427,7 @@ export async function runCodexAppServerAttempt(
notificationCleanup();
requestCleanup();
closeCleanup?.();
releaseSharedClientLeaseOnce();
releaseSharedClientLease?.();
if (nativeHookRelay) {
if (shouldDelayNativeHookRelayUnregister) {
// Codex hook subprocesses can outlive a completed app-server turn by a

View File

@@ -71,9 +71,7 @@ describe("createCodexAttemptTurnWatchController", () => {
try {
controller.armAttemptIdleWatch();
controller.touchActivity("turn:start", { attemptProgress: true });
await new Promise((resolve) => {
setTimeout(resolve, 20);
});
await new Promise((resolve) => setTimeout(resolve, 20));
controller.noteNotificationReceived("response.output_text.delta", {
attemptProgress: true,
attemptTimeoutMs: 40,
@@ -407,7 +405,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
return turnStartResult("turn-1", "inProgress");
}
if (method === "turn/interrupt") {
return new Promise<never>(() => {});
return new Promise<never>(() => undefined);
}
return {};
});
@@ -476,9 +474,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
fastWait,
);
await new Promise((resolve) => {
setTimeout(resolve, 60);
});
await new Promise((resolve) => setTimeout(resolve, 60));
await harness.notify({
method: "rawResponseItem/completed",
params: {
@@ -492,9 +488,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
},
});
await new Promise((resolve) => {
setTimeout(resolve, 60);
});
await new Promise((resolve) => setTimeout(resolve, 60));
await harness.notify({
method: "rawResponseItem/completed",
params: {
@@ -549,9 +543,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
fastWait,
);
await new Promise((resolve) => {
setTimeout(resolve, 60);
});
await new Promise((resolve) => setTimeout(resolve, 60));
await harness.handleServerRequest({
id: "request-account-refresh",
method: "account/nonTurnRefresh",
@@ -603,9 +595,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
});
await harness.waitForMethod("turn/start");
await new Promise((resolve) => {
setTimeout(resolve, 60);
});
await new Promise((resolve) => setTimeout(resolve, 60));
void harness.handleServerRequest({
id: "request-auth-refresh",
method: "account/chatgptAuthTokens/refresh",
@@ -669,9 +659,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
fastWait,
);
await new Promise((resolve) => {
setTimeout(resolve, 60);
});
await new Promise((resolve) => setTimeout(resolve, 60));
await harness.handleServerRequest({
id: "request-null-turn-elicitation",
method: "mcpServer/elicitation/request",
@@ -685,9 +673,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
_meta: null,
},
});
await new Promise((resolve) => {
setTimeout(resolve, 60);
});
await new Promise((resolve) => setTimeout(resolve, 60));
expect(harness.request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false);
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
@@ -749,9 +735,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
),
fastWait,
);
await new Promise((resolve) => {
setTimeout(resolve, 60);
});
await new Promise((resolve) => setTimeout(resolve, 60));
expect(
onRunProgress.mock.calls.some(
([event]) =>
@@ -804,9 +788,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
fastWait,
);
await new Promise((resolve) => {
setTimeout(resolve, 75);
});
await new Promise((resolve) => setTimeout(resolve, 75));
const response = harness.handleServerRequest({
id: "request-user-input",
method: "item/tool/requestUserInput",
@@ -830,9 +812,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await vi.waitFor(() => expect(params.onBlockReply).toHaveBeenCalledTimes(1), fastWait);
await new Promise((resolve) => {
setTimeout(resolve, 125);
});
await new Promise((resolve) => setTimeout(resolve, 125));
expect(harness.request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false);
expect(queueActiveRunMessageForTest("session-1", "2")).toBe(true);
@@ -863,9 +843,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
});
await harness.waitForMethod("turn/start");
await new Promise((resolve) => {
setTimeout(resolve, 60);
});
await new Promise((resolve) => setTimeout(resolve, 60));
await harness.handleServerRequest({
id: "request-foreign-elicitation",
method: "mcpServer/elicitation/request",
@@ -1074,9 +1052,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => {
setTimeout(resolve, 20);
});
await new Promise((resolve) => setTimeout(resolve, 20));
expect(settled).toBe(false);
expect(request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false);
@@ -1182,9 +1158,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => {
setTimeout(resolve, 20);
});
await new Promise((resolve) => setTimeout(resolve, 20));
expect(settled).toBe(false);
expect(request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false);
@@ -1284,9 +1258,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
},
});
await new Promise((resolve) => {
setTimeout(resolve, 20);
});
await new Promise((resolve) => setTimeout(resolve, 20));
expect(request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false);
await notify({
@@ -1370,9 +1342,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
})) as { success?: boolean };
expect(toolResult.success).toBe(false);
await new Promise((resolve) => {
setTimeout(resolve, 130);
});
await new Promise((resolve) => setTimeout(resolve, 130));
expect(settled).toBe(false);
expect(request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false);
@@ -1436,9 +1406,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => {
setTimeout(resolve, 130);
});
await new Promise((resolve) => setTimeout(resolve, 130));
expect(settled).toBe(false);
expect(harness.request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false);
@@ -1518,9 +1486,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
fastWait,
);
await new Promise((resolve) => {
setTimeout(resolve, 130);
});
await new Promise((resolve) => setTimeout(resolve, 130));
expect(settled).toBe(false);
expect(request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false);
@@ -1713,9 +1679,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => {
setTimeout(resolve, 20);
});
await new Promise((resolve) => setTimeout(resolve, 20));
expect(settled).toBe(false);
const result = await run;
@@ -1829,9 +1793,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => {
setTimeout(resolve, 40);
});
await new Promise((resolve) => setTimeout(resolve, 40));
expect(settled).toBe(false);
const result = await run;
@@ -1922,9 +1884,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => {
setTimeout(resolve, 30);
});
await new Promise((resolve) => setTimeout(resolve, 30));
// This covers the future-compatible path for raw response deltas if Codex
// app-server exposes them directly; current Codex primarily emits
// rawResponseItem/completed for the raw-event surface.
@@ -1936,9 +1896,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
delta: '{"cmd":"apply_patch","patch":"large chunk"}',
},
});
await new Promise((resolve) => {
setTimeout(resolve, 30);
});
await new Promise((resolve) => setTimeout(resolve, 30));
expect(settled).toBe(false);
await notify({
@@ -2031,9 +1989,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => {
setTimeout(resolve, 30);
});
await new Promise((resolve) => setTimeout(resolve, 30));
await notify({
method: "item/fileChange/patchUpdated",
params: {
@@ -2140,9 +2096,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => {
setTimeout(resolve, 30);
});
await new Promise((resolve) => setTimeout(resolve, 30));
await notify({
method: "response.custom_tool_call_input.delta",
params: {
@@ -2240,9 +2194,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => {
setTimeout(resolve, 40);
});
await new Promise((resolve) => setTimeout(resolve, 40));
await notify({
method: "response.custom_tool_call_input.delta",
params: {
@@ -2645,9 +2597,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => {
setTimeout(resolve, 25);
});
await new Promise((resolve) => setTimeout(resolve, 25));
expect(settled).toBe(false);
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
@@ -2700,9 +2650,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => {
setTimeout(resolve, 25);
});
await new Promise((resolve) => setTimeout(resolve, 25));
expect(settled).toBe(false);
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
@@ -2738,9 +2686,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
await new Promise((resolve) => setTimeout(resolve, 100));
expect(settled).toBe(false);
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
@@ -2794,9 +2740,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
});
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
await new Promise((resolve) => setTimeout(resolve, 100));
expect(settled).toBe(false);
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
@@ -2819,9 +2763,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
const run = runCodexAppServerAttempt(params, { turnCompletionIdleTimeoutMs: 15 });
await harness.waitForMethod("turn/start");
await harness.notify(rateLimitsUpdated(Date.now() + 60_000));
await new Promise((resolve) => {
setTimeout(resolve, 20);
});
await new Promise((resolve) => setTimeout(resolve, 20));
const result = await run;
expect({
@@ -2938,9 +2880,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
const queuedTerminal = harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
void queuedTerminal.catch(() => undefined);
await new Promise((resolve) => {
setTimeout(resolve, 30);
});
await new Promise((resolve) => setTimeout(resolve, 30));
expect(settled).toBe(false);
expect(harness.request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false);
@@ -3251,9 +3191,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
},
});
await new Promise((resolve) => {
setTimeout(resolve, 20);
});
await new Promise((resolve) => setTimeout(resolve, 20));
expect(request).not.toHaveBeenCalledWith("turn/interrupt", expect.anything());
await notify({
@@ -3334,9 +3272,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
},
});
await new Promise((resolve) => {
setTimeout(resolve, 20);
});
await new Promise((resolve) => setTimeout(resolve, 20));
expect(request).not.toHaveBeenCalledWith("turn/interrupt", expect.anything());
await notify({
@@ -3497,9 +3433,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
},
});
await new Promise((resolve) => {
setTimeout(resolve, 20);
});
await new Promise((resolve) => setTimeout(resolve, 20));
expect(request).not.toHaveBeenCalledWith("turn/interrupt", expect.anything());
await notify({
@@ -3743,9 +3677,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
);
await harness.waitForMethod("turn/start");
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
harness.close();
const result = await run;
@@ -3813,9 +3745,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
},
},
});
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
expect(resolved).toBe(false);
await harness.notify({

View File

@@ -206,9 +206,7 @@ export async function waitForHttpBodyDeltas(
if (deltas.length >= count) {
return deltas;
}
await new Promise((resolve) => {
setTimeout(resolve, 25);
});
await new Promise((resolve) => setTimeout(resolve, 25));
}
throw new Error(`expected ${count} http body deltas`);
}

View File

@@ -704,9 +704,7 @@ describe("shared Codex app-server client", () => {
});
try {
await new Promise<void>((resolve) => {
server.once("listening", resolve);
});
await new Promise<void>((resolve) => server.once("listening", resolve));
const address = server.address();
if (!address || typeof address === "string") {
throw new Error("expected websocket test server port");
@@ -743,9 +741,9 @@ describe("shared Codex app-server client", () => {
expect(authHeaders).toEqual(["Bearer tok-first", "Bearer tok-second"]);
} finally {
clearSharedCodexAppServerClient();
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
await new Promise<void>((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve())),
);
}
});
});

View File

@@ -141,9 +141,7 @@ function mockCall(mock: ReturnType<typeof vi.fn>, index = 0): unknown[] {
}
function flushDiagnosticEvents() {
return new Promise<void>((resolve) => {
setImmediate(resolve);
});
return new Promise<void>((resolve) => setImmediate(resolve));
}
function activeDiagnosticToolKeys(events: DiagnosticEventPayload[]): Set<string> {

View File

@@ -12,12 +12,14 @@ describe("Codex app-server websocket transport", () => {
}
clients.length = 0;
await Promise.all(
servers.splice(0).map(
(server) =>
new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
}),
),
servers
.splice(0)
.map(
(server) =>
new Promise<void>((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve())),
),
),
);
});
@@ -40,9 +42,7 @@ describe("Codex app-server websocket transport", () => {
}
});
});
await new Promise<void>((resolve) => {
server.once("listening", resolve);
});
await new Promise<void>((resolve) => server.once("listening", resolve));
const address = server.address();
if (!address || typeof address === "string") {
throw new Error("expected websocket test server port");

View File

@@ -1096,9 +1096,7 @@ describe("codex conversation binding", () => {
},
{ timeoutMs: 50 },
);
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
expect(result).toEqual({
handled: true,

View File

@@ -387,9 +387,7 @@ function isCodexPluginLoadWarningItem(item: MigrationItem): boolean {
}
async function sleep(ms: number): Promise<void> {
await new Promise((resolve) => {
setTimeout(resolve, ms);
});
await new Promise((resolve) => setTimeout(resolve, ms));
}
async function buildTargetCodexPluginAppCacheKey(ctx: MigrationProviderContext): Promise<string> {

View File

@@ -440,9 +440,7 @@ async function waitForLocalHistory(params: {
}
const pollDelayMs = resolveComfyRemainingMs(deadline, params.timeoutMs, params.pollIntervalMs);
await new Promise((resolve) => {
setTimeout(resolve, pollDelayMs);
});
await new Promise((resolve) => setTimeout(resolve, pollDelayMs));
}
}
@@ -481,9 +479,7 @@ async function waitForCloudCompletion(params: {
}
const pollDelayMs = resolveComfyRemainingMs(deadline, params.timeoutMs, params.pollIntervalMs);
await new Promise((resolve) => {
setTimeout(resolve, pollDelayMs);
});
await new Promise((resolve) => setTimeout(resolve, pollDelayMs));
}
}

View File

@@ -55,9 +55,7 @@ function createDeferred<T>() {
}
async function flushAsyncWork() {
await new Promise((resolve) => {
setTimeout(resolve, 0);
});
await new Promise((resolve) => setTimeout(resolve, 0));
}
describe("createCopilotAgentHarness", () => {

View File

@@ -729,9 +729,7 @@ describe("runCopilotAttempt", () => {
});
const session = await sessionCreated.promise;
for (let i = 0; i < 100 && session.sendAndWait.mock.calls.length === 0; i++) {
await new Promise((resolve) => {
setTimeout(resolve, 0);
});
await new Promise((resolve) => setTimeout(resolve, 0));
}
expect(session.sendAndWait).toHaveBeenCalledTimes(1);

View File

@@ -420,7 +420,7 @@ export function convertOpenClawToolToSdkTool(
);
}
let preparedArgs;
let preparedArgs = args;
try {
preparedArgs = sourceTool.prepareArguments ? sourceTool.prepareArguments(args) : args;
} catch (error: unknown) {

View File

@@ -346,9 +346,7 @@ async function emitAndCaptureLog(
}
function flushDiagnosticEvents() {
return new Promise<void>((resolve) => {
setImmediate(resolve);
});
return new Promise<void>((resolve) => setImmediate(resolve));
}
function emitTrustedModelCallCompletedWithContent(
@@ -3299,26 +3297,24 @@ describe("diagnostics-otel service", () => {
},
{
inputMessages: [
{ role: "user", content: "what changed?", timestamp: 1 },
{
role: "assistant",
content: [
{ type: "toolCall", id: "call-1", name: "lookup", arguments: { q: "trace" } },
],
},
{ role: "toolResult", toolCallId: "call-1", content: { rows: 1 } },
],
{ role: "user", content: "what changed?", timestamp: 1 },
{
role: "assistant",
content: [{ type: "toolCall", id: "call-1", name: "lookup", arguments: { q: "trace" } }],
},
{ role: "toolResult", toolCallId: "call-1", content: { rows: 1 } },
],
outputMessages: [
{
role: "assistant",
content: [{ type: "text", text: "the trace changed" }],
stopReason: "stop",
},
],
{
role: "assistant",
content: [{ type: "text", text: "the trace changed" }],
stopReason: "stop",
},
],
systemPrompt: "be exact",
toolDefinitions: [
{ name: "lookup", description: "Lookup data", parameters: { type: "object" } },
],
{ name: "lookup", description: "Lookup data", parameters: { type: "object" } },
],
},
);
await flushDiagnosticEvents();

View File

@@ -478,8 +478,8 @@ export function normalizeCompatibilityConfig({
}
const changes: string[] = [];
let updated;
let changed;
let updated = rawEntry;
let changed = false;
const bindingsToAdd: AgentBindingConfig[] = [];
const aliases = normalizeLegacyChannelAliases({

View File

@@ -298,7 +298,7 @@ export function createDiscordAutoPresenceController(params: {
let lastAppliedAt = 0;
const runEvaluation = (options?: { force?: boolean }) => {
let decision: DiscordAutoPresenceDecision | null;
let decision: DiscordAutoPresenceDecision | null = null;
try {
decision = resolveDiscordAutoPresenceDecision({
discordConfig: params.discordConfig,

View File

@@ -256,7 +256,7 @@ export function createDiscordDraftPreviewController(params: {
);
}
const alreadyStarted = progressDraftGate.hasStarted;
let progressActive;
let progressActive = false;
if (shouldStartDiscordProgressDraftNow(line)) {
await progressDraftGate.startNow();
progressActive = progressDraftGate.hasStarted;

View File

@@ -146,7 +146,7 @@ function copyRuntimeMessageFields(source: Message, target: Message): void {
}
function shouldHydrateDiscordMessage(params: { message: Message }) {
let currentText;
let currentText = "";
try {
currentText = resolveDiscordMessageText(params.message, {
includeForwarded: true,

View File

@@ -1154,13 +1154,9 @@ describe("processDiscordMessage ack reactions", () => {
vi.useFakeTimers();
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
await params?.replyOptions?.onCompactionStart?.();
await new Promise((resolve) => {
setTimeout(resolve, 1_000);
});
await new Promise((resolve) => setTimeout(resolve, 1_000));
await params?.replyOptions?.onCompactionEnd?.();
await new Promise((resolve) => {
setTimeout(resolve, 1_000);
});
await new Promise((resolve) => setTimeout(resolve, 1_000));
return createNoQueuedDispatchResult();
});
@@ -1549,9 +1545,7 @@ describe("processDiscordMessage session routing", () => {
vi.useFakeTimers();
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
await params?.replyOptions?.onReasoningStream?.();
await new Promise((resolve) => {
setTimeout(resolve, 1_000);
});
await new Promise((resolve) => setTimeout(resolve, 1_000));
return createNoQueuedDispatchResult();
});
const ctx = await createBaseContext({
@@ -1589,9 +1583,7 @@ describe("processDiscordMessage session routing", () => {
vi.useFakeTimers();
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
await params?.replyOptions?.onReasoningStream?.();
await new Promise((resolve) => {
setTimeout(resolve, 1_000);
});
await new Promise((resolve) => setTimeout(resolve, 1_000));
return createNoQueuedDispatchResult();
});
const ctx = await createBaseContext({

View File

@@ -119,9 +119,7 @@ export async function applyDiscordModelPickerSelection(params: {
const fallbackRoute = dispatchResult.effectiveRoute ?? params.route;
if (params.settleMs > 0) {
await new Promise((resolve) => {
setTimeout(resolve, params.settleMs);
});
await new Promise((resolve) => setTimeout(resolve, params.settleMs));
}
let effectiveModelRef = params.resolveCurrentModel(fallbackRoute);
@@ -137,9 +135,7 @@ export async function applyDiscordModelPickerSelection(params: {
params.selectedModel === params.defaultModel,
runtime: params.selectedRuntime,
});
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
await new Promise((resolve) => setTimeout(resolve, 100));
effectiveModelRef = params.resolveCurrentModel(fallbackRoute);
persisted = effectiveModelRef === params.resolvedModelRef;
}
@@ -159,9 +155,7 @@ export async function applyDiscordModelPickerSelection(params: {
params.selectedModel === params.defaultModel,
runtime: params.selectedRuntime,
});
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
await new Promise((resolve) => setTimeout(resolve, 100));
effectiveModelRef = params.resolveCurrentModel(fallbackRoute);
persisted = effectiveModelRef === params.resolvedModelRef;
if (!persisted) {

View File

@@ -336,7 +336,7 @@ export function formatDiscordDeployErrorDetails(err: unknown): string {
details.push(`code=${discordCode}`);
}
if (rawBody !== undefined) {
let bodyText;
let bodyText = "";
try {
bodyText = JSON.stringify(rawBody);
} catch {

View File

@@ -440,9 +440,7 @@ describe("createDiscordGatewayPlugin", () => {
process.on("unhandledRejection", onUnhandledRejection);
try {
startIgnoredGatewayRegistration(plugin);
await new Promise((resolve) => {
setImmediate(resolve);
});
await new Promise((resolve) => setImmediate(resolve));
expect(unhandledReasons).toHaveLength(0);
const registration = waitForDiscordGatewayPluginRegistration(plugin);

View File

@@ -3865,18 +3865,14 @@ describe("DiscordVoiceManager", () => {
resolveSecond?.({ payloads: [{ text: "second answer" }] });
resolveThird?.({ payloads: [{ text: "third answer" }] });
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
expectUserMessageNotIncludes("second answer");
expectUserMessageNotIncludes("third answer");
bridgeParams?.onEvent?.({ direction: "server", type: "response.done" });
const firstStream = lastAudioResourceInput() as PassThrough | undefined;
await vi.waitFor(() => expect(firstStream?.writableEnded).toBe(true));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
expectUserMessageNotIncludes("second answer");
const idleHandler = player.on.mock.calls.find(([event]) => event === "idle")?.[1] as
@@ -3890,9 +3886,7 @@ describe("DiscordVoiceManager", () => {
bridgeParams?.onEvent?.({ direction: "server", type: "response.done" });
const secondStream = lastAudioResourceInput() as PassThrough | undefined;
await vi.waitFor(() => expect(secondStream?.writableEnded).toBe(true));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
expectUserMessageNotIncludes("third answer");
idleHandler?.();
@@ -3956,9 +3950,7 @@ describe("DiscordVoiceManager", () => {
bridgeParams?.onEvent?.({ direction: "server", type: "response.done" });
const firstStream = lastAudioResourceInput() as PassThrough | undefined;
await vi.waitFor(() => expect(firstStream?.writableEnded).toBe(true));
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
expectUserMessageNotIncludes("second answer");
const idleHandler = player.on.mock.calls.find(([event]) => event === "idle")?.[1] as

View File

@@ -538,9 +538,7 @@ async function waitForFalQueueResult(params: {
throw new Error(FAL_VIDEO_MALFORMED_RESPONSE);
}
const pollDelayMs = resolveFalQueueRemainingMs(params.deadline, lastStatus, POLL_INTERVAL_MS);
await new Promise((resolve) => {
setTimeout(resolve, pollDelayMs);
});
await new Promise((resolve) => setTimeout(resolve, pollDelayMs));
}
}

View File

@@ -340,9 +340,7 @@ export async function getAppOwnerOpenId(params: {
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
return new Promise((resolve) => setTimeout(resolve, ms));
}
function sleepRegistrationPollInterval(intervalSeconds: number): Promise<void> {

View File

@@ -92,7 +92,7 @@ export function resolveFeishuGroupSession(params: {
(replyInThread ? messageId : null))
: null;
let peerId;
let peerId = chatId;
switch (groupSessionScope) {
case "group_sender":
peerId = buildFeishuConversationId({ chatId, scope: "group_sender", senderOpenId });

View File

@@ -64,7 +64,7 @@ export const detectFeishuLegacyStateMigrations: BundledChannelLegacyStateMigrati
stateDir,
}) => {
const dedupDir = path.join(stateDir, "feishu", "dedup");
let entries: fs.Dirent[];
let entries: fs.Dirent[] = [];
try {
entries = fs.readdirSync(dedupDir, { withFileTypes: true });
} catch {

View File

@@ -400,7 +400,7 @@ function inspectSessionTranscript(params: {
return null;
}
let raw;
let raw = "";
try {
raw = fs.readFileSync(params.transcriptPath, "utf-8");
} catch {

View File

@@ -474,7 +474,7 @@ export async function monitorSingleAccount(params: MonitorSingleAccountParams):
log(`feishu[${accountId}]: dedup warmup loaded ${warmupCount} entries from disk`);
}
let threadBindingManager: ReturnType<typeof createFeishuThreadBindingManager> | null | undefined;
let threadBindingManager: ReturnType<typeof createFeishuThreadBindingManager> | null = null;
try {
const eventDispatcher = createEventDispatcher(account);
const chatHistories = new Map<string, HistoryEntry[]>();

View File

@@ -361,9 +361,7 @@ async function resolveParsedCommentContent(params: {
}
async function delayMs(ms: number): Promise<void> {
await new Promise((resolve) => {
setTimeout(resolve, ms);
});
await new Promise((resolve) => setTimeout(resolve, ms));
}
function buildDriveCommentTargetUrl(params: {

View File

@@ -1,9 +1,5 @@
import { createServer } from "node:http";
import type { AddressInfo } from "node:net";
import {
fetchWithSsrFGuard,
ssrfPolicyFromDangerouslyAllowPrivateNetwork,
} from "openclaw/plugin-sdk/ssrf-runtime";
import { vi } from "vitest";
import type { ClawdbotConfig } from "../runtime-api.js";
import type { monitorFeishuProvider } from "./monitor.js";
@@ -14,41 +10,26 @@ const WEBHOOK_MONITOR_START_MAX_ATTEMPTS = 4;
export async function getFreePort(): Promise<number> {
const server = createServer();
await new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", () => resolve());
});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
const address = server.address() as AddressInfo | null;
if (!address) {
throw new Error("missing server address");
}
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
await new Promise<void>((resolve) => server.close(() => resolve()));
return address.port;
}
async function waitUntilServerReady(url: string): Promise<void> {
for (let i = 0; i < WEBHOOK_READY_MAX_ATTEMPTS; i += 1) {
try {
const { response, release } = await fetchWithSsrFGuard({
url,
init: { method: "GET" },
policy: ssrfPolicyFromDangerouslyAllowPrivateNetwork(true),
auditContext: "feishu-webhook-test-ready",
});
try {
if (response.status >= 200 && response.status < 500) {
return;
}
} finally {
await release();
const response = await fetch(url, { method: "GET" });
if (response.status >= 200 && response.status < 500) {
return;
}
} catch {
// retry
}
await new Promise((resolve) => {
setTimeout(resolve, WEBHOOK_READY_RETRY_DELAY_MS);
});
await new Promise((resolve) => setTimeout(resolve, WEBHOOK_READY_RETRY_DELAY_MS));
}
throw new Error(`server did not start: ${url}`);
}
@@ -127,9 +108,7 @@ export async function withRunningWebhookMonitor(
abortController.abort();
await monitorPromise.catch(() => undefined);
if (attempt < WEBHOOK_MONITOR_START_MAX_ATTEMPTS) {
await new Promise((resolve) => {
setTimeout(resolve, attempt * WEBHOOK_READY_RETRY_DELAY_MS);
});
await new Promise((resolve) => setTimeout(resolve, attempt * WEBHOOK_READY_RETRY_DELAY_MS));
}
}
}

View File

@@ -162,7 +162,6 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
resolveMarkdownTableMode: vi.fn(() => "preserve"),
convertMarkdownTables: vi.fn((text) => text),
chunkTextWithMode: vi.fn((text) => [text]),
chunkMarkdownTextWithMode: vi.fn((text) => [text]),
},
reply: {
createReplyDispatcherWithTyping: createReplyDispatcherWithTypingMock,
@@ -404,85 +403,6 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled();
});
it("keeps oversized auto mode plain final text on the chunked message path", async () => {
const runtime = getFeishuRuntimeMock();
runtime.channel.text.resolveTextChunkLimit.mockReturnValue(10);
runtime.channel.text.chunkTextWithMode.mockReturnValue(["0123456789", "abcdefghij"]);
const { options } = createDispatcherHarness();
await options.deliver({ text: "0123456789abcdefghij" }, { kind: "final" });
await options.onIdle?.();
expect(streamingInstances).toHaveLength(0);
expect(sendMessageFeishuMock).toHaveBeenCalledTimes(2);
expectMockArgFields(sendMessageFeishuMock, "first message send params", {
text: "0123456789",
});
expectMockArgFields(
sendMessageFeishuMock,
"second message send params",
{
text: "abcdefghij",
},
1,
);
expect(sendStructuredCardFeishuMock).not.toHaveBeenCalled();
});
it("keeps oversized auto mode markdown final text on the chunked card path", async () => {
const runtime = getFeishuRuntimeMock();
runtime.channel.text.resolveTextChunkLimit.mockReturnValue(10);
runtime.channel.text.chunkMarkdownTextWithMode.mockReturnValue(["```ts\nx\n```", "tail"]);
const { options } = createDispatcherHarness({ runtime: createRuntimeLogger() });
await options.deliver({ text: "```ts\nconst x = 1\n```\ntail" }, { kind: "final" });
await options.onIdle?.();
expect(streamingInstances).toHaveLength(0);
expect(runtime.channel.text.chunkMarkdownTextWithMode).toHaveBeenCalledTimes(1);
expect(runtime.channel.text.chunkTextWithMode).not.toHaveBeenCalled();
expect(sendStructuredCardFeishuMock).toHaveBeenCalledTimes(2);
expectMockArgFields(sendStructuredCardFeishuMock, "first card send params", {
text: "```ts\nx\n```",
});
expectMockArgFields(
sendStructuredCardFeishuMock,
"second card send params",
{
text: "tail",
},
1,
);
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
});
it("discards partial streaming preview before oversized final text fallback", async () => {
const runtime = getFeishuRuntimeMock();
runtime.channel.text.resolveTextChunkLimit.mockReturnValue(10);
runtime.channel.text.chunkTextWithMode.mockReturnValue(["final text", " overflow"]);
const { result, options } = createDispatcherHarness({ runtime: createRuntimeLogger() });
result.replyOptions.onPartialReply?.({ text: "partial" });
await options.deliver({ text: "final text overflow" }, { kind: "final" });
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].discard).toHaveBeenCalledTimes(1);
expect(streamingInstances[0].close).not.toHaveBeenCalled();
expect(sendMessageFeishuMock).toHaveBeenCalledTimes(2);
expectMockArgFields(sendMessageFeishuMock, "first message send params", {
text: "final text",
});
expectMockArgFields(
sendMessageFeishuMock,
"second message send params",
{
text: " overflow",
},
1,
);
});
it("keeps auto mode plain tool text on the message path when streaming is enabled", async () => {
const { options } = createDispatcherHarness();
await options.deliver({ text: "tool summary" }, { kind: "tool" });
@@ -840,33 +760,6 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
});
});
it("skips oversized late final text after streaming card close", async () => {
const runtime = getFeishuRuntimeMock();
runtime.channel.text.resolveTextChunkLimit.mockReturnValue(10);
runtime.channel.text.chunkTextWithMode.mockReturnValue(["oversized ", "late final"]);
const { options } = createDispatcherHarness({
runtime: createRuntimeLogger(),
});
await options.deliver({ text: "First" }, { kind: "final" });
await options.onIdle?.();
await options.deliver(
{ text: "oversized late final", mediaUrl: "https://example.com/a.png" },
{ kind: "final" },
);
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
expect(sendStructuredCardFeishuMock).not.toHaveBeenCalled();
expect(sendMediaFeishuMock).toHaveBeenCalledTimes(1);
expectMockArgFields(sendMediaFeishuMock, "media send params", {
mediaUrl: "https://example.com/a.png",
});
});
it("suppresses duplicate final text while still sending media", async () => {
const options = setupNonStreamingAutoDispatcher();
await options.deliver({ text: "plain final" }, { kind: "final" });
@@ -1805,9 +1698,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
const fallbackPromise = result.ensureNoVisibleReplyFallback("zero-final-count");
for (let attempt = 0; attempt < 20 && closeMock.mock.calls.length === 0; attempt += 1) {
await new Promise((resolve) => {
setTimeout(resolve, 0);
});
await new Promise((resolve) => setTimeout(resolve, 0));
}
expect(closeMock).toHaveBeenCalledTimes(1);
expect(sendMessageFeishuMock).not.toHaveBeenCalled();

View File

@@ -463,12 +463,9 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
const chunkSource = paramsLocal.useCard
? paramsLocal.text
: core.channel.text.convertMarkdownTables(paramsLocal.text, tableMode);
const chunkText = paramsLocal.useCard
? core.channel.text.chunkMarkdownTextWithMode
: core.channel.text.chunkTextWithMode;
const chunks = resolveTextChunksWithFallback(
chunkSource,
chunkText(chunkSource, textChunkLimit, chunkMode),
core.channel.text.chunkTextWithMode(chunkSource, textChunkLimit, chunkMode),
);
for (const [index, chunk] of chunks.entries()) {
await paramsLocal.sendChunk({
@@ -632,21 +629,13 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
...(payload.audioAsVoice === true ? { audioAsVoice: true } : {}),
}),
);
const finalTextExceedsStreamingLimit =
info?.kind === "final" && hasText && text.length > textChunkLimit;
const useStaticCard =
const streamingCardEnabledForReplyKind = streamingEnabled && info?.kind === "final";
const useCard =
hasText &&
(renderMode === "card" ||
(streamingCardEnabledForReplyKind ||
renderMode === "card" ||
(info?.kind === "block" && coreBlockStreamingEnabled && renderMode !== "raw") ||
(renderMode === "auto" && shouldUseCard(text)));
const useStreamingCard =
hasText &&
streamingEnabled &&
!finalTextExceedsStreamingLimit &&
(info?.kind === "final" || useStaticCard);
const finalTextWouldUseStreamingCard =
info?.kind === "final" && hasText && streamingEnabled;
const useCard = useStaticCard || useStreamingCard;
const skipTextForDuplicateFinal =
info?.kind === "final" && hasText && deliveredFinalTexts.has(text);
const skipTextForClosedStreamingFinal =
@@ -654,7 +643,8 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
hasText &&
streamingClosedForReply &&
!streamingCloseErroredForReply &&
finalTextWouldUseStreamingCard;
streamingEnabled &&
useCard;
const shouldDeliverText =
hasText &&
!hasVoiceMedia &&
@@ -662,8 +652,8 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
!skipTextForClosedStreamingFinal;
const shouldDiscardStreamingPreview =
info?.kind === "final" &&
(finalTextExceedsStreamingLimit ||
(hasMedia && ((hasVoiceMedia && !shouldDeliverText) || skipTextForDuplicateFinal)));
hasMedia &&
((hasVoiceMedia && !shouldDeliverText) || skipTextForDuplicateFinal);
if (!shouldDeliverText && !hasMedia) {
return;
@@ -677,7 +667,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
if (info?.kind === "block") {
// Drop internal block chunks unless we can safely consume them as
// streaming-card fallback content.
if (!useStreamingCard) {
if (!(streamingEnabled && useCard)) {
return;
}
startStreaming();
@@ -686,7 +676,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
}
}
if (info?.kind === "final" && useStreamingCard) {
if (info?.kind === "final" && streamingEnabled && useCard) {
startStreaming();
if (streamingStartPromise) {
await streamingStartPromise;

View File

@@ -85,9 +85,7 @@ describe("createSequentialQueue", () => {
}),
).rejects.toThrow("boom");
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await new Promise<void>((resolve) => setImmediate(resolve));
expect(unhandled).toStrictEqual([]);
await expect(enqueue("feishu:default:chat-1", async () => {})).resolves.toBeUndefined();

View File

@@ -347,7 +347,7 @@ async function runNewAppFlow(params: {
const targetAccountId = resolveDefaultFeishuAccountId(next);
// ----- QR scan flow -----
let appId: string | null;
let appId: string | null = null;
let appSecret: SecretInput | null = null;
let appSecretProbeValue: string | null = null;
let scanDomain: FeishuDomain | undefined;
@@ -366,6 +366,7 @@ async function runNewAppFlow(params: {
if (scanResult) {
appId = scanResult.appId;
appSecret = scanResult.appSecret;
appSecretProbeValue = scanResult.appSecret;
scanDomain = scanResult.domain;
scanOpenId = scanResult.openId;
} else {
@@ -420,9 +421,7 @@ async function runNewAppFlow(params: {
// ----- Apply credentials & security policy -----
const configProgress = prompter.progress(t("wizard.feishu.configuring"));
await new Promise((resolve) => {
setTimeout(resolve, 50);
});
await new Promise((resolve) => setTimeout(resolve, 50));
if (appId && appSecret) {
next = patchFeishuConfig(next, targetAccountId, {

View File

@@ -588,7 +588,7 @@ export function createDirFetchTool(): AnyAgentTool {
throw new Error(`dir.fetch UNCOMPRESSED_TOO_LARGE: ${reason}`);
};
for (const { relPath, absPath } of walked) {
let size;
let size = 0;
try {
const st = await fs.stat(absPath);
size = st.size;

View File

@@ -60,10 +60,6 @@ type GithubCopilotTestProvider = {
catalog: {
run: (ctx: unknown) => Promise<ProviderCatalogResult>;
};
resolveThinkingProfile: (ctx: {
modelId?: string;
compat?: { supportedReasoningEfforts?: readonly string[] };
}) => { levels: Array<{ id: string }> };
};
type GithubCopilotTestModelCatalogProvider = {
liveCatalog: (ctx: unknown) => Promise<readonly UnifiedModelCatalogEntry[] | null | undefined>;
@@ -184,17 +180,6 @@ describe("github-copilot plugin", () => {
expect(mocks.resolveCopilotApiToken).not.toHaveBeenCalled();
});
it("exposes xhigh thinking for catalog-supported Copilot reasoning efforts", () => {
const provider = registerProviderWithPluginConfig({});
const profile = provider.resolveThinkingProfile({
modelId: "claude-opus-4.7-1m-internal",
compat: { supportedReasoningEfforts: ["low", "medium", "high", "xhigh"] },
});
expect(profile.levels.map((level) => level.id)).toContain("xhigh");
});
it("uses live plugin config to re-enable discovery after startup disable", async () => {
mocks.resolveCopilotApiToken.mockResolvedValueOnce({
token: "copilot_api_token",

View File

@@ -42,17 +42,6 @@ type GithubCopilotPluginConfig = {
};
};
function compatSupportsXHigh(
compat: { supportedReasoningEfforts?: readonly string[] } | null | undefined,
) {
return (
Array.isArray(compat?.supportedReasoningEfforts) &&
compat.supportedReasoningEfforts.some(
(effort) => normalizeOptionalLowercaseString(effort) === "xhigh",
)
);
}
async function loadGithubCopilotRuntime() {
return await import("./register.runtime.js");
}
@@ -461,22 +450,20 @@ export default definePluginEntry({
resolveDynamicModel: (ctx) => resolveCopilotForwardCompatModel(ctx),
wrapStreamFn: wrapCopilotProviderStream,
buildReplayPolicy: ({ modelId }) => buildGithubCopilotReplayPolicy(modelId),
resolveThinkingProfile: ({ modelId, compat }) => {
const modelSupportsXHigh =
COPILOT_XHIGH_MODEL_IDS.includes(
resolveThinkingProfile: ({ modelId }) => ({
levels: [
{ id: "off" },
{ id: "minimal" },
{ id: "low" },
{ id: "medium" },
{ id: "high" },
...(COPILOT_XHIGH_MODEL_IDS.includes(
(normalizeOptionalLowercaseString(modelId) ?? "") as never,
) || compatSupportsXHigh(compat);
return {
levels: [
{ id: "off" },
{ id: "minimal" },
{ id: "low" },
{ id: "medium" },
{ id: "high" },
...(modelSupportsXHigh ? [{ id: "xhigh" as const }] : []),
],
};
},
)
? [{ id: "xhigh" as const }]
: []),
],
}),
prepareRuntimeAuth: async (ctx) => {
const { resolveCopilotApiToken } = await loadGithubCopilotRuntime();
const token = await resolveCopilotApiToken({

View File

@@ -221,9 +221,7 @@ async function sleepGitHubDevicePollDelay(delayMs: number, expiresAt: number): P
while (Date.now() < targetAt) {
const remainingMs = Math.max(1, targetAt - Date.now());
const safeDelayMs = resolveTimerTimeoutMs(remainingMs, 1);
await new Promise((resolve) => {
setTimeout(resolve, Math.min(safeDelayMs, remainingMs));
});
await new Promise((resolve) => setTimeout(resolve, Math.min(safeDelayMs, remainingMs)));
}
}

View File

@@ -11,29 +11,6 @@ const COPILOT_CHAT_COMPLETIONS_COMPAT: ModelDefinitionConfig["compat"] = {
};
const STATIC_MODEL_OVERRIDES = new Map<string, Partial<ModelDefinitionConfig>>([
[
"claude-opus-4.6-1m",
{
name: "Claude Opus 4.6 (1M context)",
api: "anthropic-messages",
reasoning: true,
contextWindow: 1_000_000,
maxTokens: 64_000,
compat: { supportedReasoningEfforts: ["low", "medium", "high"] },
},
],
[
"claude-opus-4.7-1m-internal",
{
name: "Claude Opus 4.7 (1M context)",
api: "anthropic-messages",
reasoning: true,
contextWindow: 1_000_000,
maxTokens: 64_000,
thinkingLevelMap: { xhigh: "xhigh" },
compat: { supportedReasoningEfforts: ["low", "medium", "high", "xhigh"] },
},
],
[
"gpt-5.5",
{

View File

@@ -48,9 +48,6 @@ export function buildCopilotModelDefinition(modelId: string): ModelDefinitionCon
cost: staticOverride?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: staticOverride?.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
maxTokens: staticOverride?.maxTokens ?? DEFAULT_MAX_TOKENS,
...(staticOverride?.thinkingLevelMap
? { thinkingLevelMap: staticOverride.thinkingLevelMap }
: {}),
...(compat ? { compat } : {}),
};
}

View File

@@ -104,22 +104,6 @@ describe("github-copilot model defaults", () => {
});
});
it("uses static metadata overrides for Claude Opus 1M fallback rows", () => {
const def = buildCopilotModelDefinition("claude-opus-4.7-1m-internal");
expect(def).toEqual({
id: "claude-opus-4.7-1m-internal",
name: "Claude Opus 4.7 (1M context)",
api: "anthropic-messages",
reasoning: true,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1_000_000,
maxTokens: 64_000,
thinkingLevelMap: { xhigh: "xhigh" },
compat: { supportedReasoningEfforts: ["low", "medium", "high", "xhigh"] },
});
});
it("trims whitespace from model id", () => {
const def = buildCopilotModelDefinition(" gpt-4o ");
expect(def.id).toBe("gpt-4o");
@@ -221,14 +205,6 @@ describe("resolveCopilotForwardCompatModel", () => {
});
});
it("preserves static Anthropic thinking maps for Claude Opus 1M fallback rows", () => {
const result = requireResolvedModel(createMockCtx("claude-opus-4.7-1m-internal"));
expect(result.thinkingLevelMap).toEqual({ xhigh: "xhigh" });
expect(result.compat).toEqual({
supportedReasoningEfforts: ["low", "medium", "high", "xhigh"],
});
});
it("creates synthetic model for arbitrary unknown model ID", () => {
const ctx = createMockCtx("gpt-5.4-mini");
const result = requireResolvedModel(ctx);
@@ -500,11 +476,7 @@ describe("fetchCopilotModelCatalog", () => {
max_context_window_tokens: 1000000,
max_output_tokens: 64000,
},
supports: {
vision: true,
tool_calls: true,
reasoning_effort: ["low", "medium", "high", "xhigh"],
},
supports: { vision: true, tool_calls: true },
},
},
{
@@ -568,7 +540,6 @@ describe("fetchCopilotModelCatalog", () => {
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 400000,
maxTokens: 128000,
compat: { supportedReasoningEfforts: ["low", "medium", "high"] },
});
const codex = out.find((m) => m.id === "gpt-5.3-codex");
@@ -588,10 +559,6 @@ describe("fetchCopilotModelCatalog", () => {
const opus1m = out.find((m) => m.id === "claude-opus-4.7-1m-internal");
expect(opus1m?.api).toBe("anthropic-messages");
expect(opus1m?.contextWindow).toBe(1_000_000);
expect(opus1m?.thinkingLevelMap).toEqual({ xhigh: "xhigh" });
expect(opus1m?.compat).toEqual({
supportedReasoningEfforts: ["low", "medium", "high", "xhigh"],
});
});
it("strips trailing slash from baseUrl when building the /models URL", async () => {

View File

@@ -77,9 +77,6 @@ export function resolveCopilotForwardCompatModel(
cost: staticOverride.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: staticOverride.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
maxTokens: staticOverride.maxTokens ?? DEFAULT_MAX_TOKENS,
...(staticOverride.thinkingLevelMap
? { thinkingLevelMap: staticOverride.thinkingLevelMap }
: {}),
...(compat ? { compat } : {}),
} as ProviderRuntimeModel);
}
@@ -148,41 +145,6 @@ function resolveCopilotApiForVendor(
return resolveCopilotTransportApi(modelId);
}
function mergeCopilotCompat(
base: ModelDefinitionConfig["compat"] | undefined,
reasoningEfforts: string[] | null | undefined,
): ModelDefinitionConfig["compat"] | undefined {
const supportedReasoningEfforts = Array.isArray(reasoningEfforts)
? [
...new Set(
reasoningEfforts
.map((effort) => normalizeOptionalLowercaseString(effort))
.filter((effort): effort is string => Boolean(effort)),
),
]
: [];
if (supportedReasoningEfforts.length === 0) {
return base;
}
return {
...base,
supportedReasoningEfforts,
};
}
function resolveCopilotThinkingLevelMap(
api: ModelDefinitionConfig["api"],
compat: ModelDefinitionConfig["compat"] | undefined,
): ModelDefinitionConfig["thinkingLevelMap"] | undefined {
if (
api === "anthropic-messages" &&
compat?.supportedReasoningEfforts?.some((effort) => effort === "xhigh")
) {
return { xhigh: "xhigh" };
}
return undefined;
}
function mapCopilotApiModelToDefinition(
entry: CopilotApiModelEntry,
): ModelDefinitionConfig | undefined {
@@ -212,20 +174,17 @@ function mapCopilotApiModelToDefinition(
const contextWindow =
asPositiveSafeInteger(limits?.max_context_window_tokens) ?? DEFAULT_CONTEXT_WINDOW;
const maxTokens = asPositiveSafeInteger(limits?.max_output_tokens) ?? DEFAULT_MAX_TOKENS;
const compat = mergeCopilotCompat(resolveCopilotModelCompat(id), supports?.reasoning_effort);
const api = resolveCopilotApiForVendor(entry.vendor, id);
const thinkingLevelMap = resolveCopilotThinkingLevelMap(api, compat);
const compat = resolveCopilotModelCompat(id);
const definition: ModelDefinitionConfig = {
id,
name: entry.name?.trim() || id,
api,
api: resolveCopilotApiForVendor(entry.vendor, id),
reasoning,
input,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow,
maxTokens,
...(thinkingLevelMap ? { thinkingLevelMap } : {}),
...(compat ? { compat } : {}),
};
return definition;

View File

@@ -140,9 +140,7 @@ function startGoogleMeetNodeAudioInputLoop(params: {
if (consecutiveInputErrors >= 5 || /unknown bridgeId|bridge is not open/i.test(message)) {
await params.stop();
} else {
await new Promise((resolve) => {
setTimeout(resolve, 250);
});
await new Promise((resolve) => setTimeout(resolve, 250));
}
}
}

View File

@@ -766,9 +766,7 @@ async function openMeetWithBrowserRequest(params: {
}
const remainingWaitMs = deadline - Date.now();
if (remainingWaitMs > 0) {
await new Promise((resolve) => {
setTimeout(resolve, Math.min(750, remainingWaitMs));
});
await new Promise((resolve) => setTimeout(resolve, Math.min(750, remainingWaitMs)));
}
} while (Date.now() < deadline);
return { launched: true, browser };

View File

@@ -86,16 +86,6 @@ describe("google generative ai helpers", () => {
});
it("normalizes transport baseUrls only for Google Generative AI", () => {
expect(
resolveGoogleGenerativeAiTransport({
provider: "google",
api: undefined,
baseUrl: "https://generativelanguage.googleapis.com",
}),
).toEqual({
api: "google-generative-ai",
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
});
expect(
resolveGoogleGenerativeAiTransport({
api: "google-generative-ai",

View File

@@ -267,9 +267,7 @@ async function waitForGeminiBatch(params: {
throw new Error(`gemini batch ${params.batchName} timed out after ${params.timeoutMs}ms`);
}
params.debug?.(`gemini batch ${params.batchName} ${state}; waiting ${params.pollIntervalMs}ms`);
await new Promise((resolve) => {
setTimeout(resolve, params.pollIntervalMs);
});
await new Promise((resolve) => setTimeout(resolve, params.pollIntervalMs));
current = undefined;
}
}

View File

@@ -64,9 +64,7 @@ async function pollOperation(
headers: Record<string, string>,
): Promise<{ done?: boolean; response?: { cloudaicompanionProject?: { id?: string } } }> {
for (let attempt = 0; attempt < 24; attempt += 1) {
await new Promise((resolve) => {
setTimeout(resolve, 5000);
});
await new Promise((resolve) => setTimeout(resolve, 5000));
const response = await fetchWithTimeout(`${endpoint}/v1internal/${operationName}`, {
headers,
});

Some files were not shown because too many files have changed in this diff Show More