mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-15 10:38:52 +08:00
Compare commits
155 Commits
fix-window
...
fix-exec-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c642e1da24 | ||
|
|
6316648bab | ||
|
|
bf777b9af2 | ||
|
|
fba9eac7eb | ||
|
|
5965522af5 | ||
|
|
f18fd2094f | ||
|
|
770ee8eba6 | ||
|
|
b891d42f3a | ||
|
|
705bdcec70 | ||
|
|
db7aff8843 | ||
|
|
d30329fb0e | ||
|
|
c7f3d60722 | ||
|
|
0ffaeb1273 | ||
|
|
c43a571170 | ||
|
|
dd8b9bdcb8 | ||
|
|
399f55e511 | ||
|
|
7e654b40b8 | ||
|
|
7b119ec60d | ||
|
|
c1fffe1074 | ||
|
|
530f3aaab7 | ||
|
|
3ec1a25de4 | ||
|
|
5a6ec67eb0 | ||
|
|
0fdca6974d | ||
|
|
dc344a33fb | ||
|
|
e4a766f2f4 | ||
|
|
ad07ba141d | ||
|
|
bd78737f94 | ||
|
|
5f6e608c60 | ||
|
|
ddbd16a04a | ||
|
|
03151a2ebe | ||
|
|
1b69e7a005 | ||
|
|
227530f906 | ||
|
|
6df3fd5730 | ||
|
|
7c315252d6 | ||
|
|
0d7abcc94f | ||
|
|
344773ba09 | ||
|
|
ae4550f48b | ||
|
|
fdd02444b7 | ||
|
|
3491834d49 | ||
|
|
12cf34a8ea | ||
|
|
d328a0d7a0 | ||
|
|
421ad93203 | ||
|
|
dc05f598bb | ||
|
|
3171278372 | ||
|
|
01193dea26 | ||
|
|
cb9847968a | ||
|
|
54987715f3 | ||
|
|
0c74f18a1c | ||
|
|
59122812c0 | ||
|
|
bc95af1b7c | ||
|
|
144405e562 | ||
|
|
290b19275b | ||
|
|
72f74b33e1 | ||
|
|
bb673f47b2 | ||
|
|
16ef9c1435 | ||
|
|
2b30951b80 | ||
|
|
56b8030cd9 | ||
|
|
5706619068 | ||
|
|
edc0a22179 | ||
|
|
2682c02774 | ||
|
|
59683978e1 | ||
|
|
c8f8907f15 | ||
|
|
8eb1838dfa | ||
|
|
01f6ad6056 | ||
|
|
b7f657b3b0 | ||
|
|
22cb7fb6b7 | ||
|
|
48afba96a3 | ||
|
|
470a1ae8d1 | ||
|
|
a2acfc5049 | ||
|
|
fe8c781d67 | ||
|
|
ac2484f23e | ||
|
|
cabfbdfe0d | ||
|
|
5e2472567a | ||
|
|
79c4ac73d7 | ||
|
|
2a1882ebcc | ||
|
|
3bb04b67e9 | ||
|
|
cd0a7b10e2 | ||
|
|
bc45c36dbc | ||
|
|
7184522fae | ||
|
|
aa74d93aff | ||
|
|
be0d3489a6 | ||
|
|
f06b4b9aab | ||
|
|
0700f13d62 | ||
|
|
3c6c247e0a | ||
|
|
2e42b1372e | ||
|
|
f78bb34cb4 | ||
|
|
85c7490f72 | ||
|
|
63d93db867 | ||
|
|
2976db4b2c | ||
|
|
025bb01268 | ||
|
|
7a292bb16e | ||
|
|
a9e3eade5d | ||
|
|
3733cd8d63 | ||
|
|
190f935b53 | ||
|
|
c21e16c73d | ||
|
|
d52f1ea5ec | ||
|
|
13967e17e6 | ||
|
|
7ad2aa44dd | ||
|
|
874b3f921e | ||
|
|
c11d5d6d65 | ||
|
|
11631bf044 | ||
|
|
561e993282 | ||
|
|
23bf48e69e | ||
|
|
7d65ea3513 | ||
|
|
bfac12a184 | ||
|
|
cdcc151145 | ||
|
|
7681b95199 | ||
|
|
caa08a6dc0 | ||
|
|
4339d7c1d8 | ||
|
|
aa187c6496 | ||
|
|
34010894c1 | ||
|
|
c74bb4475a | ||
|
|
299a023bd1 | ||
|
|
0c852036c7 | ||
|
|
9cc759dd37 | ||
|
|
d1378650bb | ||
|
|
40f99e474a | ||
|
|
dc71b5867e | ||
|
|
fd2c65f59b | ||
|
|
575f74293e | ||
|
|
b27ae3f6e7 | ||
|
|
b388d3dc71 | ||
|
|
01b7ef9e88 | ||
|
|
4b89def277 | ||
|
|
fabd9469cd | ||
|
|
d3025b4007 | ||
|
|
c06096eabc | ||
|
|
9577e0be5a | ||
|
|
b12724b79b | ||
|
|
0de60cec12 | ||
|
|
c6232347dc | ||
|
|
b73e135f97 | ||
|
|
9b6c981260 | ||
|
|
02ac0ec48b | ||
|
|
d8329dedf6 | ||
|
|
b86e8bf359 | ||
|
|
3bb9224836 | ||
|
|
fdc10a64e9 | ||
|
|
87174c80b6 | ||
|
|
97c040f946 | ||
|
|
f833e96a31 | ||
|
|
9a32c0f85d | ||
|
|
d306f5bf2e | ||
|
|
65d5f7436c | ||
|
|
b78ce079a3 | ||
|
|
6c6cf41b14 | ||
|
|
0d79cbab4e | ||
|
|
b04c3e96d6 | ||
|
|
3854a61bea | ||
|
|
0d07e30725 | ||
|
|
bfc151e9d3 | ||
|
|
b653d94918 | ||
|
|
49e5091f18 | ||
|
|
cbdb59b255 | ||
|
|
2ac2a8d210 |
@@ -4,11 +4,11 @@ profile: openclaw-check
|
||||
provider: azure
|
||||
class: standard
|
||||
capacity:
|
||||
market: spot
|
||||
market: on-demand
|
||||
strategy: most-available
|
||||
# Fail closed instead of silently falling back to on-demand while the
|
||||
# Azure-backed billing account is the default runner path.
|
||||
fallback: spot-only
|
||||
# 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.
|
||||
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 has reliable D2 spot capacity in eastus2;
|
||||
# eastus rejects the same SKUs and can stall provisioning.
|
||||
# The OpenClaw Azure subscription is reliable in eastus2; eastus rejects the
|
||||
# same SKUs and can stall provisioning.
|
||||
location: eastus2
|
||||
sync:
|
||||
delete: true
|
||||
@@ -71,14 +71,16 @@ env:
|
||||
- OPENCLAW_*
|
||||
ssh:
|
||||
user: crabbox
|
||||
port: "2222"
|
||||
# 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"
|
||||
jobs:
|
||||
prewarm:
|
||||
provider: azure
|
||||
target: linux
|
||||
class: standard
|
||||
type: Standard_D2ads_v6
|
||||
market: spot
|
||||
type: Standard_D4ads_v6
|
||||
market: on-demand
|
||||
idleTimeout: 90m
|
||||
hydrate:
|
||||
actions: true
|
||||
@@ -95,8 +97,8 @@ jobs:
|
||||
provider: azure
|
||||
target: linux
|
||||
class: standard
|
||||
type: Standard_D2ads_v6
|
||||
market: spot
|
||||
type: Standard_D4ads_v6
|
||||
market: on-demand
|
||||
idleTimeout: 90m
|
||||
hydrate:
|
||||
actions: true
|
||||
@@ -105,7 +107,18 @@ jobs:
|
||||
workflow: .github/workflows/crabbox-hydrate.yml
|
||||
job: hydrate
|
||||
ref: main
|
||||
command: env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 CI=1 corepack pnpm check:changed
|
||||
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
|
||||
stop: always
|
||||
testbox-changed:
|
||||
provider: blacksmith-testbox
|
||||
|
||||
136
.github/workflows/ci-check-testbox.yml
vendored
136
.github/workflows/ci-check-testbox.yml
vendored
@@ -139,3 +139,139 @@ 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"
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"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",
|
||||
@@ -35,6 +36,7 @@
|
||||
"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",
|
||||
|
||||
@@ -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 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: 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: 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.
|
||||
|
||||
@@ -5528,6 +5528,7 @@ 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
|
||||
@@ -5552,6 +5553,7 @@ public struct SkillsProposalRecordResult: Codable, Sendable {
|
||||
createdat: String,
|
||||
updatedat: String,
|
||||
createdby: AnyCodable,
|
||||
origin: [String: AnyCodable]?,
|
||||
proposedversion: String,
|
||||
draftfile: String,
|
||||
drafthash: String,
|
||||
@@ -5575,6 +5577,7 @@ 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
|
||||
@@ -5600,6 +5603,7 @@ 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"
|
||||
|
||||
@@ -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 at `~/.openclaw/state/imessage/reply-cache.jsonl` (mode `0600`, parent dir `0700`). It is safe to delete if you want a clean slate.
|
||||
The reply cache lives in SQLite plugin state. `openclaw doctor --fix` imports and archives the old `imessage/reply-cache.jsonl` sidecar when present.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -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 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`.
|
||||
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`.
|
||||
|
||||
</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 at `<openclawStateDir>/imessage/catchup/<account>__<hash>.json` (the OpenClaw state dir defaults to `~/.openclaw`, overridable with `OPENCLAW_STATE_DIR`):
|
||||
Catchup keeps a per-account cursor in SQLite plugin state:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -729,6 +729,7 @@ Catchup keeps a per-account cursor at `<openclawStateDir>/imessage/catchup/<acco
|
||||
- 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
|
||||
|
||||
|
||||
@@ -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.
|
||||
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.
|
||||
</ParamField>
|
||||
<ParamField path="onSubagentEnded" type="method">
|
||||
Clean up when a subagent session completes or is swept.
|
||||
|
||||
@@ -76,8 +76,7 @@ Notes:
|
||||
extra approval scopes:
|
||||
- commandless request: `operator.pairing`
|
||||
- non-exec command request: `operator.pairing` + `operator.write`
|
||||
- `system.run` / `system.run.prepare` / `system.which` /
|
||||
`system.execApprovals.*` request:
|
||||
- `system.run` / `system.run.prepare` / `system.which` request:
|
||||
`operator.pairing` + `operator.admin`
|
||||
|
||||
<Warning>
|
||||
|
||||
@@ -417,13 +417,6 @@ 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).
|
||||
|
||||
|
||||
@@ -262,7 +262,12 @@ 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) {
|
||||
@@ -302,7 +307,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 {
|
||||
|
||||
@@ -1196,7 +1196,7 @@ export class AcpxRuntime implements AcpRuntime {
|
||||
const record = await this.sessionStore.load(
|
||||
input.handle.acpxRecordId ?? input.handle.sessionKey,
|
||||
);
|
||||
let closeSucceeded = false;
|
||||
let closeSucceeded;
|
||||
try {
|
||||
await this.resolveDelegateForLoadedRecord(input.handle, record).close({
|
||||
handle: input.handle,
|
||||
|
||||
@@ -2958,7 +2958,9 @@ 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 },
|
||||
@@ -3001,7 +3003,9 @@ 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" }] };
|
||||
});
|
||||
|
||||
@@ -3131,7 +3135,9 @@ 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." }] };
|
||||
});
|
||||
|
||||
@@ -3221,7 +3227,9 @@ 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." }] };
|
||||
});
|
||||
|
||||
|
||||
@@ -42,7 +42,9 @@ 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` };
|
||||
}
|
||||
@@ -55,7 +57,9 @@ 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;
|
||||
}
|
||||
});
|
||||
@@ -307,7 +311,9 @@ 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) => {
|
||||
@@ -341,7 +347,9 @@ 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(
|
||||
|
||||
@@ -397,7 +397,9 @@ 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 {
|
||||
|
||||
@@ -79,7 +79,9 @@ 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) => {
|
||||
@@ -113,7 +115,9 @@ 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;
|
||||
}
|
||||
});
|
||||
@@ -1072,7 +1076,9 @@ 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) => {
|
||||
@@ -1110,7 +1116,9 @@ 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({
|
||||
@@ -1119,8 +1127,12 @@ 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());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -27,7 +27,9 @@ 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;
|
||||
};
|
||||
|
||||
@@ -77,7 +79,9 @@ 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;
|
||||
};
|
||||
|
||||
@@ -85,14 +89,16 @@ describe("cdp", () => {
|
||||
vi.unstubAllEnvs();
|
||||
await new Promise<void>((resolve) => {
|
||||
if (!httpServer) {
|
||||
return resolve();
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
httpServer.close(() => resolve());
|
||||
httpServer = null;
|
||||
});
|
||||
await new Promise<void>((resolve) => {
|
||||
if (!wsServer) {
|
||||
return resolve();
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
wsServer.close(() => resolve());
|
||||
wsServer = null;
|
||||
@@ -190,7 +196,9 @@ 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(
|
||||
@@ -210,7 +218,9 @@ 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 {
|
||||
@@ -507,7 +517,9 @@ 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({
|
||||
@@ -516,8 +528,12 @@ 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());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ async function diagnoseCdpHealthCommand(
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
let parsed: { id?: unknown; result?: unknown } | null = null;
|
||||
let parsed: { id?: unknown; result?: unknown } | null;
|
||||
try {
|
||||
parsed = JSON.parse(rawDataToString(raw)) as { id?: unknown; result?: unknown };
|
||||
} catch {
|
||||
|
||||
@@ -194,8 +194,12 @@ 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());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -952,9 +956,13 @@ 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);
|
||||
@@ -962,7 +970,9 @@ 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
|
||||
@@ -972,7 +982,9 @@ 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());
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -994,9 +1006,13 @@ 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" });
|
||||
@@ -1009,14 +1025,18 @@ 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());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -42,14 +42,12 @@ 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()));
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -108,8 +108,12 @@ 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());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,7 +553,9 @@ describe("browser chrome helpers", () => {
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BrowserCdpEndpointBlockedError);
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
await new Promise<void>((resolve) => {
|
||||
server.close(() => resolve());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -559,7 +565,7 @@ describe("browser chrome helpers", () => {
|
||||
onConnection: (wss) => {
|
||||
wss.on("connection", (ws) => {
|
||||
ws.on("message", (raw) => {
|
||||
let message: { id?: unknown; method?: unknown } | null = null;
|
||||
let message: { id?: unknown; method?: unknown } | null;
|
||||
try {
|
||||
const text =
|
||||
typeof raw === "string"
|
||||
@@ -755,8 +761,12 @@ 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());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -785,12 +795,16 @@ 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());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -811,7 +825,9 @@ 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);
|
||||
@@ -820,7 +836,9 @@ 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());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -519,7 +519,9 @@ 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");
|
||||
@@ -531,7 +533,9 @@ 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -577,7 +581,9 @@ 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) {
|
||||
@@ -682,7 +688,9 @@ 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 {
|
||||
|
||||
@@ -37,7 +37,9 @@ 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(
|
||||
@@ -78,7 +80,9 @@ 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());
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -212,7 +212,9 @@ 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");
|
||||
|
||||
@@ -947,7 +947,9 @@ 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) {
|
||||
@@ -1066,7 +1068,7 @@ async function findPageByTargetId(
|
||||
const pages = await getAllPages(browser);
|
||||
let resolvedViaCdp = false;
|
||||
for (const page of pages) {
|
||||
let tid: string | null = null;
|
||||
let tid: string | null;
|
||||
try {
|
||||
tid = await pageTargetId(page);
|
||||
resolvedViaCdp = true;
|
||||
@@ -1170,7 +1172,7 @@ export async function getPageForTargetId(opts: {
|
||||
}
|
||||
|
||||
function isTopLevelNavigationRequest(page: Page, request: Request): boolean {
|
||||
let sameMainFrame = false;
|
||||
let sameMainFrame;
|
||||
try {
|
||||
sameMainFrame = request.frame() === page.mainFrame();
|
||||
} catch {
|
||||
@@ -1197,7 +1199,7 @@ function isTopLevelNavigationRequest(page: Page, request: Request): boolean {
|
||||
}
|
||||
|
||||
function isSubframeDocumentNavigationRequest(page: Page, request: Request): boolean {
|
||||
let sameMainFrame = false;
|
||||
let sameMainFrame;
|
||||
try {
|
||||
sameMainFrame = request.frame() === page.mainFrame();
|
||||
} catch {
|
||||
|
||||
@@ -581,7 +581,9 @@ 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(
|
||||
|
||||
@@ -45,7 +45,9 @@ 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;
|
||||
|
||||
@@ -34,7 +34,9 @@ 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;
|
||||
|
||||
@@ -286,7 +286,9 @@ 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(
|
||||
@@ -306,7 +308,9 @@ 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));
|
||||
};
|
||||
|
||||
@@ -350,7 +350,9 @@ 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({
|
||||
|
||||
@@ -21,7 +21,9 @@ 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: {
|
||||
|
||||
@@ -41,7 +41,9 @@ 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 () => {
|
||||
|
||||
@@ -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 = null;
|
||||
let status: BrowserStatus | null;
|
||||
|
||||
try {
|
||||
status = await fetchBrowserStatus(parent, profile);
|
||||
|
||||
@@ -171,7 +171,7 @@ export async function handleBrowserGatewayRequest({
|
||||
}
|
||||
|
||||
const cfg = getRuntimeConfig();
|
||||
let nodeTarget: NodeSession | null = null;
|
||||
let nodeTarget: NodeSession | null;
|
||||
try {
|
||||
nodeTarget = resolveBrowserNodeTarget({
|
||||
cfg,
|
||||
|
||||
@@ -388,7 +388,6 @@ 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);
|
||||
@@ -398,7 +397,6 @@ describe("canvas host", () => {
|
||||
}
|
||||
|
||||
await fs.symlink(path.join(process.cwd(), "package.json"), linkPath);
|
||||
createdLink = true;
|
||||
|
||||
try {
|
||||
const res = await captureA2uiResponse(`${A2UI_PATH}/`);
|
||||
@@ -421,9 +419,7 @@ describe("canvas host", () => {
|
||||
expect(symlinkRes.status).toBe(404);
|
||||
expect(symlinkRes.body).toBe("not found");
|
||||
} finally {
|
||||
if (createdLink) {
|
||||
await fs.rm(linkPath, { force: true });
|
||||
}
|
||||
await fs.rm(linkPath, { force: true });
|
||||
if (createdBundle) {
|
||||
await fs.rm(bundlePath, { force: true });
|
||||
}
|
||||
|
||||
@@ -443,7 +443,9 @@ export async function createCanvasHostHandler(
|
||||
}
|
||||
}
|
||||
if (wss) {
|
||||
await new Promise<void>((resolve) => wss.close(() => resolve()));
|
||||
await new Promise<void>((resolve) => {
|
||||
wss.close(() => resolve());
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -528,9 +530,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()));
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -121,7 +121,9 @@ 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",
|
||||
|
||||
@@ -190,7 +190,9 @@ 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 });
|
||||
|
||||
@@ -790,7 +790,9 @@ 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}`);
|
||||
@@ -838,10 +840,14 @@ 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 () => {
|
||||
@@ -930,7 +936,9 @@ 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",
|
||||
|
||||
@@ -337,7 +337,6 @@ 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",
|
||||
|
||||
@@ -147,7 +147,7 @@ describe("Codex app-server attempt timeouts", () => {
|
||||
}, 5);
|
||||
});
|
||||
},
|
||||
operation: async () => new Promise<never>(() => undefined),
|
||||
operation: async () => new Promise<never>(() => {}),
|
||||
});
|
||||
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>(() => undefined),
|
||||
operation: async () => new Promise<never>(() => {}),
|
||||
});
|
||||
const rejected = expect(run).rejects.toThrow("codex app-server startup aborted");
|
||||
|
||||
|
||||
@@ -486,7 +486,7 @@ describe("CodexAppServerClient", () => {
|
||||
clients.push(harness.client);
|
||||
harness.client.addRequestHandler((request) => {
|
||||
if (request.method === "item/tool/call") {
|
||||
return new Promise<never>(() => undefined);
|
||||
return new Promise<never>(() => {});
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
@@ -179,6 +179,32 @@ 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) => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
buildAgentHookContextChannelFields,
|
||||
buildEmbeddedAttemptToolRunContext,
|
||||
embeddedAgentLog,
|
||||
filterProviderNormalizableTools,
|
||||
isSubagentSessionKey,
|
||||
normalizeAgentRuntimeTools,
|
||||
resolveAttemptSpawnWorkspaceDir,
|
||||
@@ -9,6 +10,7 @@ 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";
|
||||
@@ -265,15 +267,19 @@ 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(allTools)
|
||||
: filterCodexDynamicTools(allTools, input.pluginConfig),
|
||||
allTools,
|
||||
? filterCodexMemoryFlushDynamicTools(readableAllTools)
|
||||
: filterCodexDynamicTools(readableAllTools, input.pluginConfig),
|
||||
readableAllTools,
|
||||
input,
|
||||
),
|
||||
allTools,
|
||||
readableAllTools,
|
||||
input,
|
||||
);
|
||||
toolBuildStages.mark("codex-filtering");
|
||||
@@ -295,8 +301,25 @@ 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";
|
||||
@@ -308,7 +331,7 @@ export async function buildDynamicTools(input: DynamicToolBuildParams) {
|
||||
phase,
|
||||
totalMs: summary.totalMs,
|
||||
stages: summary.stages,
|
||||
allToolCount: allTools.length,
|
||||
allToolCount: readableAllTools.length,
|
||||
codexFilteredToolCount: codexFilteredTools.length,
|
||||
visionFilteredToolCount: visionFilteredTools.length,
|
||||
filteredToolCount: filteredTools.length,
|
||||
|
||||
@@ -194,7 +194,7 @@ describe("dynamic tool execution helpers", () => {
|
||||
toolBridge: {
|
||||
handleToolCall: vi.fn((_call, options) => {
|
||||
capturedSignal = options?.signal;
|
||||
return new Promise<never>(() => undefined);
|
||||
return new Promise<never>(() => {});
|
||||
}),
|
||||
},
|
||||
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>(() => undefined)),
|
||||
handleToolCall: vi.fn(() => new Promise<never>(() => {})),
|
||||
},
|
||||
signal: new AbortController().signal,
|
||||
timeoutMs: 1,
|
||||
|
||||
@@ -35,7 +35,9 @@ 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) {
|
||||
|
||||
@@ -52,8 +52,30 @@ 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: vi.fn(),
|
||||
createRunningTaskRun,
|
||||
tryCreateRunningTaskRun: vi.fn((params) => createRunningTaskRun(params)),
|
||||
recordTaskRunProgressByRunId: vi.fn(() => []),
|
||||
finalizeTaskRunByRunId: vi.fn(() => []),
|
||||
listTaskRecords: vi.fn((): AgentHarnessTaskRecord[] => []),
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
|
||||
function createRuntime() {
|
||||
return {
|
||||
createRunningTaskRun: vi.fn(),
|
||||
tryCreateRunningTaskRun: vi.fn((params) => ({ taskId: "task-native-subagent", ...params })),
|
||||
recordTaskRunProgressByRunId: vi.fn(() => []),
|
||||
finalizeTaskRunByRunId: vi.fn(() => []),
|
||||
} as unknown as TaskLifecycleRuntime;
|
||||
@@ -49,7 +49,7 @@ describe("CodexNativeSubagentTaskMirror", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(runtime.createRunningTaskRun).toHaveBeenCalledWith({
|
||||
expect(runtime.tryCreateRunningTaskRun).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.createRunningTaskRun).mock.calls[0]?.[0]).not.toHaveProperty(
|
||||
expect(vi.mocked(runtime.tryCreateRunningTaskRun).mock.calls[0]?.[0]).not.toHaveProperty(
|
||||
"childSessionKey",
|
||||
);
|
||||
expect(runtime.recordTaskRunProgressByRunId).toHaveBeenCalledWith({
|
||||
@@ -99,7 +99,7 @@ describe("CodexNativeSubagentTaskMirror", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(runtime.createRunningTaskRun).not.toHaveBeenCalled();
|
||||
expect(runtime.tryCreateRunningTaskRun).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.createRunningTaskRun).toHaveBeenCalledTimes(1);
|
||||
expect(runtime.tryCreateRunningTaskRun).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("maps Codex thread status changes onto the mirrored task run", () => {
|
||||
@@ -228,7 +228,7 @@ describe("CodexNativeSubagentTaskMirror", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(runtime.createRunningTaskRun).toHaveBeenCalledWith({
|
||||
expect(runtime.tryCreateRunningTaskRun).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.createRunningTaskRun).mock.calls[0]?.[0]).not.toHaveProperty(
|
||||
expect(vi.mocked(runtime.tryCreateRunningTaskRun).mock.calls[0]?.[0]).not.toHaveProperty(
|
||||
"childSessionKey",
|
||||
);
|
||||
expect(runtime.recordTaskRunProgressByRunId).toHaveBeenCalledWith({
|
||||
@@ -282,7 +282,7 @@ describe("CodexNativeSubagentTaskMirror", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(runtime.createRunningTaskRun).toHaveBeenCalledWith(
|
||||
expect(runtime.tryCreateRunningTaskRun).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
runId: "codex-thread:child-thread",
|
||||
task: "inspect one thing",
|
||||
@@ -319,7 +319,7 @@ describe("CodexNativeSubagentTaskMirror", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(runtime.createRunningTaskRun).toHaveBeenCalledWith(
|
||||
expect(runtime.tryCreateRunningTaskRun).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
runId: "codex-thread:child-thread",
|
||||
task: "inspect one thing",
|
||||
|
||||
@@ -15,7 +15,7 @@ import { isJsonObject } from "./protocol.js";
|
||||
|
||||
export type TaskLifecycleRuntime = Pick<
|
||||
AgentHarnessTaskRuntime,
|
||||
"createRunningTaskRun" | "recordTaskRunProgressByRunId" | "finalizeTaskRunByRunId"
|
||||
"tryCreateRunningTaskRun" | "recordTaskRunProgressByRunId" | "finalizeTaskRunByRunId"
|
||||
>;
|
||||
|
||||
export type CodexNativeSubagentTaskMirrorParams = {
|
||||
@@ -27,6 +27,7 @@ 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;
|
||||
|
||||
@@ -81,7 +82,7 @@ export class CodexNativeSubagentTaskMirror {
|
||||
trimOptional(thread.preview) ??
|
||||
`Codex native subagent${label === "Codex subagent" ? "" : ` ${label}`}`;
|
||||
const createdAt = secondsToMillis(thread.createdAt) ?? this.now();
|
||||
this.runtime.createRunningTaskRun({
|
||||
const taskRecord = this.runtime.tryCreateRunningTaskRun({
|
||||
sourceId: runId,
|
||||
agentId: this.params.agentId,
|
||||
runId,
|
||||
@@ -94,6 +95,13 @@ 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);
|
||||
}
|
||||
|
||||
@@ -106,6 +114,9 @@ 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;
|
||||
@@ -219,7 +230,7 @@ export class CodexNativeSubagentTaskMirror {
|
||||
const prompt = trimOptional(readString(item, "prompt"));
|
||||
const runId = codexNativeSubagentRunId(normalizedThreadId);
|
||||
const createdAt = this.now();
|
||||
this.runtime.createRunningTaskRun({
|
||||
const taskRecord = this.runtime.tryCreateRunningTaskRun({
|
||||
sourceId: runId,
|
||||
agentId: this.params.agentId,
|
||||
runId,
|
||||
@@ -232,6 +243,13 @@ 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(
|
||||
@@ -239,6 +257,9 @@ 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;
|
||||
|
||||
@@ -85,7 +85,9 @@ 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);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,9 @@ 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 [
|
||||
{
|
||||
|
||||
@@ -817,7 +817,7 @@ describe("runCodexAppServerAttempt", () => {
|
||||
onTimeout: async () => {
|
||||
await releaseCodexSandboxExecServerEnvironment(sandbox);
|
||||
},
|
||||
operation: async () => new Promise<never>(() => undefined),
|
||||
operation: async () => new Promise<never>(() => {}),
|
||||
}),
|
||||
).rejects.toThrow("codex app-server startup timed out");
|
||||
|
||||
@@ -1111,7 +1111,9 @@ 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;
|
||||
|
||||
@@ -1684,7 +1686,9 @@ 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;
|
||||
|
||||
@@ -1725,7 +1729,9 @@ 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;
|
||||
|
||||
@@ -1762,7 +1768,9 @@ 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;
|
||||
|
||||
@@ -1801,7 +1809,9 @@ 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;
|
||||
|
||||
@@ -1846,7 +1856,9 @@ 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;
|
||||
|
||||
@@ -1891,7 +1903,9 @@ 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;
|
||||
|
||||
@@ -2134,7 +2148,9 @@ 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;
|
||||
|
||||
@@ -2189,7 +2205,9 @@ 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;
|
||||
|
||||
@@ -2266,7 +2284,9 @@ 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;
|
||||
|
||||
@@ -2454,7 +2474,9 @@ 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;
|
||||
|
||||
@@ -2483,7 +2505,9 @@ 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;
|
||||
|
||||
@@ -2526,7 +2550,9 @@ 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;
|
||||
|
||||
@@ -2566,7 +2592,9 @@ 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;
|
||||
|
||||
@@ -2609,7 +2637,9 @@ 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;
|
||||
|
||||
@@ -2851,7 +2881,9 @@ 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);
|
||||
@@ -2943,7 +2975,9 @@ 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;
|
||||
@@ -2983,7 +3017,9 @@ 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" });
|
||||
|
||||
@@ -3076,7 +3112,9 @@ describe("runCodexAppServerAttempt", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
await new Promise<void>((resolve) => {
|
||||
setImmediate(resolve);
|
||||
});
|
||||
expect(resolved).toBe(false);
|
||||
|
||||
await harness.notify({
|
||||
@@ -3120,7 +3158,9 @@ 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]) =>
|
||||
@@ -3800,7 +3840,7 @@ describe("runCodexAppServerAttempt", () => {
|
||||
});
|
||||
|
||||
it("times out app-server startup before thread setup can hang forever", async () => {
|
||||
setCodexAppServerClientFactoryForTest(() => new Promise<never>(() => undefined));
|
||||
setCodexAppServerClientFactoryForTest(() => new Promise<never>(() => {}));
|
||||
const params = createParams(
|
||||
path.join(tempDir, "session.jsonl"),
|
||||
path.join(tempDir, "workspace"),
|
||||
@@ -3834,7 +3874,9 @@ 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;
|
||||
|
||||
@@ -4307,7 +4349,7 @@ describe("runCodexAppServerAttempt", () => {
|
||||
const c = {
|
||||
request: vi.fn(async (method: string) => {
|
||||
if (method === "thread/start") {
|
||||
return await new Promise<never>(() => undefined);
|
||||
return await new Promise<never>(() => {});
|
||||
}
|
||||
return {};
|
||||
}),
|
||||
@@ -4502,7 +4544,9 @@ 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;
|
||||
|
||||
|
||||
@@ -965,8 +965,19 @@ 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) {
|
||||
@@ -1914,7 +1925,7 @@ export async function runCodexAppServerAttempt(
|
||||
aborted: runAbortController.signal.aborted,
|
||||
promptError: turnStartErrorMessage,
|
||||
});
|
||||
trajectoryEndRecorded = true;
|
||||
markTrajectoryEndRecorded();
|
||||
runAgentHarnessLlmOutputHook({
|
||||
event: {
|
||||
runId: params.runId,
|
||||
@@ -1979,8 +1990,7 @@ export async function runCodexAppServerAttempt(
|
||||
},
|
||||
});
|
||||
params.abortSignal?.removeEventListener("abort", abortFromUpstream);
|
||||
releaseSharedClientLease?.();
|
||||
releaseSharedClientLease = undefined;
|
||||
releaseSharedClientLeaseOnce();
|
||||
if (usageLimitError) {
|
||||
await markCodexAuthProfileBlockedFromRateLimits({
|
||||
params,
|
||||
@@ -2000,8 +2010,7 @@ export async function runCodexAppServerAttempt(
|
||||
}
|
||||
}
|
||||
if (!turn) {
|
||||
releaseSharedClientLease?.();
|
||||
releaseSharedClientLease = undefined;
|
||||
releaseSharedClientLeaseOnce();
|
||||
throw new Error("codex app-server turn/start failed without an error");
|
||||
}
|
||||
turnIdRef.current = turn.turn.id;
|
||||
@@ -2250,7 +2259,7 @@ export async function runCodexAppServerAttempt(
|
||||
yieldDetected,
|
||||
promptError: normalizeCodexTrajectoryError(finalPromptError),
|
||||
});
|
||||
trajectoryEndRecorded = true;
|
||||
markTrajectoryEndRecorded();
|
||||
await mirrorTranscriptBestEffort({
|
||||
params,
|
||||
agentId: sessionAgentId,
|
||||
@@ -2427,7 +2436,7 @@ export async function runCodexAppServerAttempt(
|
||||
notificationCleanup();
|
||||
requestCleanup();
|
||||
closeCleanup?.();
|
||||
releaseSharedClientLease?.();
|
||||
releaseSharedClientLeaseOnce();
|
||||
if (nativeHookRelay) {
|
||||
if (shouldDelayNativeHookRelayUnregister) {
|
||||
// Codex hook subprocesses can outlive a completed app-server turn by a
|
||||
|
||||
@@ -71,7 +71,9 @@ 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,
|
||||
@@ -405,7 +407,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
|
||||
return turnStartResult("turn-1", "inProgress");
|
||||
}
|
||||
if (method === "turn/interrupt") {
|
||||
return new Promise<never>(() => undefined);
|
||||
return new Promise<never>(() => {});
|
||||
}
|
||||
return {};
|
||||
});
|
||||
@@ -474,7 +476,9 @@ 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: {
|
||||
@@ -488,7 +492,9 @@ 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: {
|
||||
@@ -543,7 +549,9 @@ 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",
|
||||
@@ -595,7 +603,9 @@ 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",
|
||||
@@ -659,7 +669,9 @@ 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",
|
||||
@@ -673,7 +685,9 @@ 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" });
|
||||
@@ -735,7 +749,9 @@ 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]) =>
|
||||
@@ -788,7 +804,9 @@ 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",
|
||||
@@ -812,7 +830,9 @@ 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);
|
||||
@@ -843,7 +863,9 @@ 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",
|
||||
@@ -1052,7 +1074,9 @@ 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);
|
||||
|
||||
@@ -1158,7 +1182,9 @@ 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);
|
||||
|
||||
@@ -1258,7 +1284,9 @@ 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({
|
||||
@@ -1342,7 +1370,9 @@ 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);
|
||||
|
||||
@@ -1406,7 +1436,9 @@ 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);
|
||||
|
||||
@@ -1486,7 +1518,9 @@ 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);
|
||||
|
||||
@@ -1679,7 +1713,9 @@ 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;
|
||||
@@ -1793,7 +1829,9 @@ 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;
|
||||
@@ -1884,7 +1922,9 @@ 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.
|
||||
@@ -1896,7 +1936,9 @@ 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({
|
||||
@@ -1989,7 +2031,9 @@ 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: {
|
||||
@@ -2096,7 +2140,9 @@ 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: {
|
||||
@@ -2194,7 +2240,9 @@ 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: {
|
||||
@@ -2597,7 +2645,9 @@ 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" });
|
||||
@@ -2650,7 +2700,9 @@ 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" });
|
||||
@@ -2686,7 +2738,9 @@ 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" });
|
||||
@@ -2740,7 +2794,9 @@ 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" });
|
||||
@@ -2763,7 +2819,9 @@ 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({
|
||||
@@ -2880,7 +2938,9 @@ 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);
|
||||
@@ -3191,7 +3251,9 @@ 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({
|
||||
@@ -3272,7 +3334,9 @@ 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({
|
||||
@@ -3433,7 +3497,9 @@ 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({
|
||||
@@ -3677,7 +3743,9 @@ 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;
|
||||
@@ -3745,7 +3813,9 @@ 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({
|
||||
|
||||
@@ -206,7 +206,9 @@ 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`);
|
||||
}
|
||||
|
||||
@@ -704,7 +704,9 @@ 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");
|
||||
@@ -741,9 +743,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()));
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -141,7 +141,9 @@ 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> {
|
||||
|
||||
@@ -12,14 +12,12 @@ 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()));
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -42,7 +40,9 @@ 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");
|
||||
|
||||
@@ -1096,7 +1096,9 @@ 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,
|
||||
|
||||
@@ -387,7 +387,9 @@ 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> {
|
||||
|
||||
@@ -440,7 +440,9 @@ 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,7 +481,9 @@ 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,9 @@ function createDeferred<T>() {
|
||||
}
|
||||
|
||||
async function flushAsyncWork() {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
}
|
||||
|
||||
describe("createCopilotAgentHarness", () => {
|
||||
|
||||
@@ -729,7 +729,9 @@ 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);
|
||||
|
||||
|
||||
@@ -420,7 +420,7 @@ export function convertOpenClawToolToSdkTool(
|
||||
);
|
||||
}
|
||||
|
||||
let preparedArgs = args;
|
||||
let preparedArgs;
|
||||
try {
|
||||
preparedArgs = sourceTool.prepareArguments ? sourceTool.prepareArguments(args) : args;
|
||||
} catch (error: unknown) {
|
||||
|
||||
@@ -346,7 +346,9 @@ async function emitAndCaptureLog(
|
||||
}
|
||||
|
||||
function flushDiagnosticEvents() {
|
||||
return new Promise<void>((resolve) => setImmediate(resolve));
|
||||
return new Promise<void>((resolve) => {
|
||||
setImmediate(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function emitTrustedModelCallCompletedWithContent(
|
||||
@@ -3297,24 +3299,26 @@ 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();
|
||||
|
||||
@@ -478,8 +478,8 @@ export function normalizeCompatibilityConfig({
|
||||
}
|
||||
|
||||
const changes: string[] = [];
|
||||
let updated = rawEntry;
|
||||
let changed = false;
|
||||
let updated;
|
||||
let changed;
|
||||
const bindingsToAdd: AgentBindingConfig[] = [];
|
||||
|
||||
const aliases = normalizeLegacyChannelAliases({
|
||||
|
||||
@@ -298,7 +298,7 @@ export function createDiscordAutoPresenceController(params: {
|
||||
let lastAppliedAt = 0;
|
||||
|
||||
const runEvaluation = (options?: { force?: boolean }) => {
|
||||
let decision: DiscordAutoPresenceDecision | null = null;
|
||||
let decision: DiscordAutoPresenceDecision | null;
|
||||
try {
|
||||
decision = resolveDiscordAutoPresenceDecision({
|
||||
discordConfig: params.discordConfig,
|
||||
|
||||
@@ -256,7 +256,7 @@ export function createDiscordDraftPreviewController(params: {
|
||||
);
|
||||
}
|
||||
const alreadyStarted = progressDraftGate.hasStarted;
|
||||
let progressActive = false;
|
||||
let progressActive;
|
||||
if (shouldStartDiscordProgressDraftNow(line)) {
|
||||
await progressDraftGate.startNow();
|
||||
progressActive = progressDraftGate.hasStarted;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1154,9 +1154,13 @@ 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();
|
||||
});
|
||||
|
||||
@@ -1545,7 +1549,9 @@ 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({
|
||||
@@ -1583,7 +1589,9 @@ 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({
|
||||
|
||||
@@ -119,7 +119,9 @@ 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);
|
||||
@@ -135,7 +137,9 @@ 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;
|
||||
}
|
||||
@@ -155,7 +159,9 @@ 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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -440,7 +440,9 @@ 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);
|
||||
|
||||
@@ -3865,14 +3865,18 @@ 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
|
||||
@@ -3886,7 +3890,9 @@ 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?.();
|
||||
@@ -3950,7 +3956,9 @@ 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
|
||||
|
||||
@@ -538,7 +538,9 @@ 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -340,7 +340,9 @@ 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> {
|
||||
|
||||
@@ -92,7 +92,7 @@ export function resolveFeishuGroupSession(params: {
|
||||
(replyInThread ? messageId : null))
|
||||
: null;
|
||||
|
||||
let peerId = chatId;
|
||||
let peerId;
|
||||
switch (groupSessionScope) {
|
||||
case "group_sender":
|
||||
peerId = buildFeishuConversationId({ chatId, scope: "group_sender", senderOpenId });
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -400,7 +400,7 @@ function inspectSessionTranscript(params: {
|
||||
return null;
|
||||
}
|
||||
|
||||
let raw = "";
|
||||
let raw;
|
||||
try {
|
||||
raw = fs.readFileSync(params.transcriptPath, "utf-8");
|
||||
} catch {
|
||||
|
||||
@@ -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 = null;
|
||||
let threadBindingManager: ReturnType<typeof createFeishuThreadBindingManager> | null | undefined;
|
||||
try {
|
||||
const eventDispatcher = createEventDispatcher(account);
|
||||
const chatHistories = new Map<string, HistoryEntry[]>();
|
||||
|
||||
@@ -361,7 +361,9 @@ 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: {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
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";
|
||||
@@ -10,26 +14,41 @@ 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 = await fetch(url, { method: "GET" });
|
||||
if (response.status >= 200 && response.status < 500) {
|
||||
return;
|
||||
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();
|
||||
}
|
||||
} 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}`);
|
||||
}
|
||||
@@ -108,7 +127,9 @@ 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +162,7 @@ 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,
|
||||
@@ -403,6 +404,85 @@ 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" });
|
||||
@@ -760,6 +840,33 @@ 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" });
|
||||
@@ -1698,7 +1805,9 @@ 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();
|
||||
|
||||
@@ -463,9 +463,12 @@ 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,
|
||||
core.channel.text.chunkTextWithMode(chunkSource, textChunkLimit, chunkMode),
|
||||
chunkText(chunkSource, textChunkLimit, chunkMode),
|
||||
);
|
||||
for (const [index, chunk] of chunks.entries()) {
|
||||
await paramsLocal.sendChunk({
|
||||
@@ -629,13 +632,21 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
||||
...(payload.audioAsVoice === true ? { audioAsVoice: true } : {}),
|
||||
}),
|
||||
);
|
||||
const streamingCardEnabledForReplyKind = streamingEnabled && info?.kind === "final";
|
||||
const useCard =
|
||||
const finalTextExceedsStreamingLimit =
|
||||
info?.kind === "final" && hasText && text.length > textChunkLimit;
|
||||
const useStaticCard =
|
||||
hasText &&
|
||||
(streamingCardEnabledForReplyKind ||
|
||||
renderMode === "card" ||
|
||||
(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 =
|
||||
@@ -643,8 +654,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
||||
hasText &&
|
||||
streamingClosedForReply &&
|
||||
!streamingCloseErroredForReply &&
|
||||
streamingEnabled &&
|
||||
useCard;
|
||||
finalTextWouldUseStreamingCard;
|
||||
const shouldDeliverText =
|
||||
hasText &&
|
||||
!hasVoiceMedia &&
|
||||
@@ -652,8 +662,8 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
||||
!skipTextForClosedStreamingFinal;
|
||||
const shouldDiscardStreamingPreview =
|
||||
info?.kind === "final" &&
|
||||
hasMedia &&
|
||||
((hasVoiceMedia && !shouldDeliverText) || skipTextForDuplicateFinal);
|
||||
(finalTextExceedsStreamingLimit ||
|
||||
(hasMedia && ((hasVoiceMedia && !shouldDeliverText) || skipTextForDuplicateFinal)));
|
||||
|
||||
if (!shouldDeliverText && !hasMedia) {
|
||||
return;
|
||||
@@ -667,7 +677,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 (!(streamingEnabled && useCard)) {
|
||||
if (!useStreamingCard) {
|
||||
return;
|
||||
}
|
||||
startStreaming();
|
||||
@@ -676,7 +686,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
||||
}
|
||||
}
|
||||
|
||||
if (info?.kind === "final" && streamingEnabled && useCard) {
|
||||
if (info?.kind === "final" && useStreamingCard) {
|
||||
startStreaming();
|
||||
if (streamingStartPromise) {
|
||||
await streamingStartPromise;
|
||||
|
||||
@@ -85,7 +85,9 @@ 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();
|
||||
|
||||
@@ -347,7 +347,7 @@ async function runNewAppFlow(params: {
|
||||
const targetAccountId = resolveDefaultFeishuAccountId(next);
|
||||
|
||||
// ----- QR scan flow -----
|
||||
let appId: string | null = null;
|
||||
let appId: string | null;
|
||||
let appSecret: SecretInput | null = null;
|
||||
let appSecretProbeValue: string | null = null;
|
||||
let scanDomain: FeishuDomain | undefined;
|
||||
@@ -366,7 +366,6 @@ async function runNewAppFlow(params: {
|
||||
if (scanResult) {
|
||||
appId = scanResult.appId;
|
||||
appSecret = scanResult.appSecret;
|
||||
appSecretProbeValue = scanResult.appSecret;
|
||||
scanDomain = scanResult.domain;
|
||||
scanOpenId = scanResult.openId;
|
||||
} else {
|
||||
@@ -421,7 +420,9 @@ 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, {
|
||||
|
||||
@@ -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 = 0;
|
||||
let size;
|
||||
try {
|
||||
const st = await fs.stat(absPath);
|
||||
size = st.size;
|
||||
|
||||
@@ -60,6 +60,10 @@ 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>;
|
||||
@@ -180,6 +184,17 @@ 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",
|
||||
|
||||
@@ -42,6 +42,17 @@ 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");
|
||||
}
|
||||
@@ -450,20 +461,22 @@ export default definePluginEntry({
|
||||
resolveDynamicModel: (ctx) => resolveCopilotForwardCompatModel(ctx),
|
||||
wrapStreamFn: wrapCopilotProviderStream,
|
||||
buildReplayPolicy: ({ modelId }) => buildGithubCopilotReplayPolicy(modelId),
|
||||
resolveThinkingProfile: ({ modelId }) => ({
|
||||
levels: [
|
||||
{ id: "off" },
|
||||
{ id: "minimal" },
|
||||
{ id: "low" },
|
||||
{ id: "medium" },
|
||||
{ id: "high" },
|
||||
...(COPILOT_XHIGH_MODEL_IDS.includes(
|
||||
resolveThinkingProfile: ({ modelId, compat }) => {
|
||||
const modelSupportsXHigh =
|
||||
COPILOT_XHIGH_MODEL_IDS.includes(
|
||||
(normalizeOptionalLowercaseString(modelId) ?? "") as never,
|
||||
)
|
||||
? [{ id: "xhigh" as const }]
|
||||
: []),
|
||||
],
|
||||
}),
|
||||
) || compatSupportsXHigh(compat);
|
||||
return {
|
||||
levels: [
|
||||
{ id: "off" },
|
||||
{ id: "minimal" },
|
||||
{ id: "low" },
|
||||
{ id: "medium" },
|
||||
{ id: "high" },
|
||||
...(modelSupportsXHigh ? [{ id: "xhigh" as const }] : []),
|
||||
],
|
||||
};
|
||||
},
|
||||
prepareRuntimeAuth: async (ctx) => {
|
||||
const { resolveCopilotApiToken } = await loadGithubCopilotRuntime();
|
||||
const token = await resolveCopilotApiToken({
|
||||
|
||||
@@ -221,7 +221,9 @@ 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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,29 @@ 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",
|
||||
{
|
||||
|
||||
@@ -48,6 +48,9 @@ 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 } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -104,6 +104,22 @@ 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");
|
||||
@@ -205,6 +221,14 @@ 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);
|
||||
@@ -476,7 +500,11 @@ describe("fetchCopilotModelCatalog", () => {
|
||||
max_context_window_tokens: 1000000,
|
||||
max_output_tokens: 64000,
|
||||
},
|
||||
supports: { vision: true, tool_calls: true },
|
||||
supports: {
|
||||
vision: true,
|
||||
tool_calls: true,
|
||||
reasoning_effort: ["low", "medium", "high", "xhigh"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -540,6 +568,7 @@ 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");
|
||||
@@ -559,6 +588,10 @@ 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 () => {
|
||||
|
||||
@@ -77,6 +77,9 @@ 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);
|
||||
}
|
||||
@@ -145,6 +148,41 @@ 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 {
|
||||
@@ -174,17 +212,20 @@ 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 = resolveCopilotModelCompat(id);
|
||||
const compat = mergeCopilotCompat(resolveCopilotModelCompat(id), supports?.reasoning_effort);
|
||||
const api = resolveCopilotApiForVendor(entry.vendor, id);
|
||||
const thinkingLevelMap = resolveCopilotThinkingLevelMap(api, compat);
|
||||
|
||||
const definition: ModelDefinitionConfig = {
|
||||
id,
|
||||
name: entry.name?.trim() || id,
|
||||
api: resolveCopilotApiForVendor(entry.vendor, id),
|
||||
api,
|
||||
reasoning,
|
||||
input,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow,
|
||||
maxTokens,
|
||||
...(thinkingLevelMap ? { thinkingLevelMap } : {}),
|
||||
...(compat ? { compat } : {}),
|
||||
};
|
||||
return definition;
|
||||
|
||||
@@ -140,7 +140,9 @@ 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -766,7 +766,9 @@ 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 };
|
||||
|
||||
@@ -86,6 +86,16 @@ 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",
|
||||
|
||||
@@ -267,7 +267,9 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,9 @@ 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
Reference in New Issue
Block a user