mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-10-18 20:04:04 +00:00
fix(skills): record timeouts as timeout and clear the race timer (#16216)
Some checks failed
Release-Green (continuous) / Baseline headroom (push) Has been cancelled
opencode-plugin CI / Test v2 (Node 22) (push) Has been cancelled
opencode-plugin CI / Test v2 (Node 24) (push) Has been cancelled
Publish to Docker Hub / Resolve Docker release metadata (push) Has been cancelled
Publish to Docker Hub / omni-build runner offline alert (push) Has been cancelled
Release-Green (continuous) / Validate active release branch (push) Has been cancelled
Release-Green (continuous) / Validate main branch (push) Has been cancelled
Release-Green (continuous) / Bank ratchet shrinks (push) Has been cancelled
Quality Gates / Change Classification (push) Has been cancelled
opencode-plugin CI / Test (Node 24) (push) Has been cancelled
CI / Change Classification (push) Has been cancelled
CI / PR Test Policy (push) Has been cancelled
Lock released branch / lock-branch (push) Has been cancelled
Lock released branch / guard-no-push-after-release (push) Has been cancelled
opencode-plugin CI / Test (Node 22) (push) Has been cancelled
OpenSSF Scorecard / Scorecard analysis (push) Has been cancelled
Quality Gates / Merge integrity (changelog + generated skills) (push) Has been cancelled
Release acceptance / Release acceptance (push) Has been cancelled
CI / Protocol Clients E2E (live server, advisory) (push) Has been cancelled
opencode-plugin CI / Build (push) Has been cancelled
CI / Gate / CI (push) Has been cancelled
CI / CI Dashboard (push) Has been cancelled
Publish to Docker Hub / Preflight — amd64 runner availability (push) Has been cancelled
Publish to Docker Hub / Build Docker (linux/amd64) (push) Has been cancelled
Publish to Docker Hub / Build Docker (linux/arm64) (push) Has been cancelled
Publish to Docker Hub / Publish multi-arch manifests (push) Has been cancelled
Quality Gates / Docs Gates (fast-path) (push) Has been cancelled
Quality Gates / Fast Quality Gates (push) Has been cancelled
Quality Gates / Vitest (fast-path) (push) Has been cancelled
Quality Gates / No new ESLint warnings (push) Has been cancelled
CI / i18n Glossary (zh-CN, ko) (push) Has been cancelled
CI / i18n Validation (all languages) (push) Has been cancelled
CI / Security Tests (push) Has been cancelled
CI / Ecosystem E2E (live server) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Quality Ratchet (push) Has been cancelled
CI / Quality Gates (Extended) (push) Has been cancelled
CI / Docs Sync (Strict) (push) Has been cancelled
CI / Docs Lint (prose — advisory) (push) Has been cancelled
CI / i18n UI Coverage (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Package Artifact (push) Has been cancelled
CI / Electron Package Smoke (ubuntu-latest) (push) Has been cancelled
CI / Electron Package Smoke (windows-latest) (push) Has been cancelled
CI / Unit Tests (1/8) (push) Has been cancelled
CI / Unit Tests (2/8) (push) Has been cancelled
CI / Unit Tests (3/8) (push) Has been cancelled
CI / Unit Tests (4/8) (push) Has been cancelled
CI / Unit Tests (5/8) (push) Has been cancelled
CI / Unit Tests (6/8) (push) Has been cancelled
CI / Unit Tests (7/8) (push) Has been cancelled
CI / Unit Tests (8/8) (push) Has been cancelled
CI / Bun SQLite Compatibility (push) Has been cancelled
CI / Bun SQLite Compatibility-1 (push) Has been cancelled
CI / Vitest (MCP / autoCombo / UI components) (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / SonarQube (push) Has been cancelled
CI / PR Coverage Comment (push) Has been cancelled
CI / E2E Tests (1/9) (push) Has been cancelled
CI / E2E Tests (2/9) (push) Has been cancelled
CI / E2E Tests (3/9) (push) Has been cancelled
CI / E2E Tests (4/9) (push) Has been cancelled
CI / E2E Tests (5/9) (push) Has been cancelled
CI / E2E Tests (6/9) (push) Has been cancelled
CI / E2E Tests (7/9) (push) Has been cancelled
CI / E2E Tests (8/9) (push) Has been cancelled
CI / E2E Tests (9/9) (push) Has been cancelled
CI / Integration Tests (1/2) (push) Has been cancelled
CI / Integration Tests (2/2) (push) Has been cancelled
Quality Gates / Unit Tests fast-path (1/4) (push) Has been cancelled
Quality Gates / Unit Tests fast-path (2/4) (push) Has been cancelled
Quality Gates / Unit Tests fast-path (3/4) (push) Has been cancelled
Quality Gates / Unit Tests fast-path (4/4) (push) Has been cancelled
Quality Gates / Gate / Quality (push) Has been cancelled
Some checks failed
Release-Green (continuous) / Baseline headroom (push) Has been cancelled
opencode-plugin CI / Test v2 (Node 22) (push) Has been cancelled
opencode-plugin CI / Test v2 (Node 24) (push) Has been cancelled
Publish to Docker Hub / Resolve Docker release metadata (push) Has been cancelled
Publish to Docker Hub / omni-build runner offline alert (push) Has been cancelled
Release-Green (continuous) / Validate active release branch (push) Has been cancelled
Release-Green (continuous) / Validate main branch (push) Has been cancelled
Release-Green (continuous) / Bank ratchet shrinks (push) Has been cancelled
Quality Gates / Change Classification (push) Has been cancelled
opencode-plugin CI / Test (Node 24) (push) Has been cancelled
CI / Change Classification (push) Has been cancelled
CI / PR Test Policy (push) Has been cancelled
Lock released branch / lock-branch (push) Has been cancelled
Lock released branch / guard-no-push-after-release (push) Has been cancelled
opencode-plugin CI / Test (Node 22) (push) Has been cancelled
OpenSSF Scorecard / Scorecard analysis (push) Has been cancelled
Quality Gates / Merge integrity (changelog + generated skills) (push) Has been cancelled
Release acceptance / Release acceptance (push) Has been cancelled
CI / Protocol Clients E2E (live server, advisory) (push) Has been cancelled
opencode-plugin CI / Build (push) Has been cancelled
CI / Gate / CI (push) Has been cancelled
CI / CI Dashboard (push) Has been cancelled
Publish to Docker Hub / Preflight — amd64 runner availability (push) Has been cancelled
Publish to Docker Hub / Build Docker (linux/amd64) (push) Has been cancelled
Publish to Docker Hub / Build Docker (linux/arm64) (push) Has been cancelled
Publish to Docker Hub / Publish multi-arch manifests (push) Has been cancelled
Quality Gates / Docs Gates (fast-path) (push) Has been cancelled
Quality Gates / Fast Quality Gates (push) Has been cancelled
Quality Gates / Vitest (fast-path) (push) Has been cancelled
Quality Gates / No new ESLint warnings (push) Has been cancelled
CI / i18n Glossary (zh-CN, ko) (push) Has been cancelled
CI / i18n Validation (all languages) (push) Has been cancelled
CI / Security Tests (push) Has been cancelled
CI / Ecosystem E2E (live server) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Quality Ratchet (push) Has been cancelled
CI / Quality Gates (Extended) (push) Has been cancelled
CI / Docs Sync (Strict) (push) Has been cancelled
CI / Docs Lint (prose — advisory) (push) Has been cancelled
CI / i18n UI Coverage (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Package Artifact (push) Has been cancelled
CI / Electron Package Smoke (ubuntu-latest) (push) Has been cancelled
CI / Electron Package Smoke (windows-latest) (push) Has been cancelled
CI / Unit Tests (1/8) (push) Has been cancelled
CI / Unit Tests (2/8) (push) Has been cancelled
CI / Unit Tests (3/8) (push) Has been cancelled
CI / Unit Tests (4/8) (push) Has been cancelled
CI / Unit Tests (5/8) (push) Has been cancelled
CI / Unit Tests (6/8) (push) Has been cancelled
CI / Unit Tests (7/8) (push) Has been cancelled
CI / Unit Tests (8/8) (push) Has been cancelled
CI / Bun SQLite Compatibility (push) Has been cancelled
CI / Bun SQLite Compatibility-1 (push) Has been cancelled
CI / Vitest (MCP / autoCombo / UI components) (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / SonarQube (push) Has been cancelled
CI / PR Coverage Comment (push) Has been cancelled
CI / E2E Tests (1/9) (push) Has been cancelled
CI / E2E Tests (2/9) (push) Has been cancelled
CI / E2E Tests (3/9) (push) Has been cancelled
CI / E2E Tests (4/9) (push) Has been cancelled
CI / E2E Tests (5/9) (push) Has been cancelled
CI / E2E Tests (6/9) (push) Has been cancelled
CI / E2E Tests (7/9) (push) Has been cancelled
CI / E2E Tests (8/9) (push) Has been cancelled
CI / E2E Tests (9/9) (push) Has been cancelled
CI / Integration Tests (1/2) (push) Has been cancelled
CI / Integration Tests (2/2) (push) Has been cancelled
Quality Gates / Unit Tests fast-path (1/4) (push) Has been cancelled
Quality Gates / Unit Tests fast-path (2/4) (push) Has been cancelled
Quality Gates / Unit Tests fast-path (3/4) (push) Has been cancelled
Quality Gates / Unit Tests fast-path (4/4) (push) Has been cancelled
Quality Gates / Gate / Quality (push) Has been cancelled
Thanks @Louayzouaoui1. Wave 27 boarded this pull request cleanly on release/v3.8.52 at f8d11c9326. The focused unit tests for the files it touches passed on that combined tree (116 passed in the second run, plus the auto-combo weight tests and the skills characterization tests). Changelog integrity passed. The only file-size finding is executeTargetAttempt.ts, already over its frozen ceiling on the release tip and unchanged by this diff.
⚠️ base-red inherited: #15306
Closes #16187
This commit is contained in:
parent
02a1204f14
commit
7f3cbb046b
5 changed files with 81 additions and 15 deletions
1
changelog.d/fixes/0000-skills-timeout-status.md
Normal file
1
changelog.d/fixes/0000-skills-timeout-status.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
- **fix(skills):** a skill that outlives the executor timeout is now recorded as `timeout` instead of `error`, so execution history and analytics can tell timeouts from handler failures. The race timer is also cleared once the handler settles; it used to stay pending for the full timeout (30 s by default) after every execution.
|
||||
|
|
@ -13,6 +13,13 @@ import { logger } from "../../../open-sse/utils/logger.ts";
|
|||
|
||||
const log = logger("SKILLS_EXECUTOR");
|
||||
|
||||
class SkillTimeoutError extends Error {
|
||||
constructor() {
|
||||
super("Skill execution timed out");
|
||||
this.name = "SkillTimeoutError";
|
||||
}
|
||||
}
|
||||
|
||||
function toSafeSkillErrorMessage(value: unknown): string {
|
||||
try {
|
||||
const raw = value instanceof Error ? value.message : value;
|
||||
|
|
@ -286,12 +293,15 @@ class SkillExecutor {
|
|||
}
|
||||
|
||||
private async executeWithTimeout<T>(promise: Promise<T>): Promise<T> {
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise<T>((_, reject) =>
|
||||
setTimeout(() => reject(new Error("Skill execution timed out")), this.timeout)
|
||||
),
|
||||
]);
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
const timeout = new Promise<never>((_, reject) => {
|
||||
timer = setTimeout(() => reject(new SkillTimeoutError()), this.timeout);
|
||||
});
|
||||
try {
|
||||
return await Promise.race([promise, timeout]);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -343,7 +353,7 @@ class SkillExecutor {
|
|||
}
|
||||
} catch (err) {
|
||||
errorMessage = toSafeSkillErrorMessage(err);
|
||||
status = SkillStatus.ERROR;
|
||||
status = err instanceof SkillTimeoutError ? SkillStatus.TIMEOUT : SkillStatus.ERROR;
|
||||
}
|
||||
|
||||
return { output, errorMessage, status };
|
||||
|
|
|
|||
|
|
@ -31,8 +31,7 @@ function uniqueName(prefix: string) {
|
|||
return `${prefix}-${randomUUID().slice(0, 8)}`;
|
||||
}
|
||||
|
||||
// Short executor timeout so the timeout case runs fast. NOTE: executeWithTimeout never
|
||||
// clears its timer, so every execution leaves a pending timer of this length behind.
|
||||
// Short executor timeout so the timeout case runs fast.
|
||||
skillExecutor.setTimeout(200);
|
||||
|
||||
skillExecutor.registerHandler("char-echo", async (input) => ({ echoed: String(input.text) }));
|
||||
|
|
@ -323,7 +322,7 @@ test("execute: a failure-shaped output and a throwing handler both resolve with
|
|||
assert.equal(hardRun.output, null);
|
||||
});
|
||||
|
||||
test("characterization: a timed-out skill is currently recorded as status error, never timeout", async () => {
|
||||
test("a timed-out skill is recorded as status timeout", async () => {
|
||||
const name = uniqueName("char-slow");
|
||||
await skillRegistry.register({
|
||||
name,
|
||||
|
|
@ -332,9 +331,8 @@ test("characterization: a timed-out skill is currently recorded as status error,
|
|||
apiKeyId: "char-slow",
|
||||
});
|
||||
const run = await skillExecutor.execute(name, {}, { apiKeyId: "char-slow" });
|
||||
// SkillStatus.TIMEOUT exists and the DB CHECK allows 'timeout', but the executor maps a
|
||||
// timeout to ERROR with this message.
|
||||
assert.equal(run.status, SkillStatus.ERROR);
|
||||
// A timeout is recorded as SkillStatus.TIMEOUT (#16187), not as a generic handler error.
|
||||
assert.equal(run.status, SkillStatus.TIMEOUT);
|
||||
assert.equal(run.errorMessage, "Skill execution timed out");
|
||||
});
|
||||
|
||||
|
|
|
|||
57
tests/unit/skills-executor-timeout.test.ts
Normal file
57
tests/unit/skills-executor-timeout.test.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import "../_setup/isolateDataDir.ts";
|
||||
|
||||
process.env.DISABLE_SQLITE_AUTO_BACKUP ||= "true";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const { skillRegistry } = await import("../../src/lib/skills/registry.ts");
|
||||
const { skillExecutor } = await import("../../src/lib/skills/executor.ts");
|
||||
const { SkillStatus } = await import("../../src/lib/skills/types.ts");
|
||||
|
||||
const SCHEMA = { input: {}, output: {} };
|
||||
const API_KEY_ID = "executor-timeout";
|
||||
|
||||
skillExecutor.registerHandler("timeout-fast", async () => ({ ok: true }));
|
||||
skillExecutor.registerHandler("timeout-slow", () => new Promise(() => {}));
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
});
|
||||
|
||||
function pendingTimers(): number {
|
||||
return process.getActiveResourcesInfo().filter((name) => name === "Timeout").length;
|
||||
}
|
||||
|
||||
async function registerSkill(handler: string) {
|
||||
const name = `${handler}-${randomUUID().slice(0, 8)}`;
|
||||
await skillRegistry.register({ name, schema: SCHEMA, handler, apiKeyId: API_KEY_ID });
|
||||
return name;
|
||||
}
|
||||
|
||||
test("a skill that outlives the executor timeout is recorded as timeout and persisted that way", async () => {
|
||||
skillExecutor.setTimeout(100);
|
||||
const name = await registerSkill("timeout-slow");
|
||||
const run = await skillExecutor.execute(name, {}, { apiKeyId: API_KEY_ID });
|
||||
assert.equal(run.status, SkillStatus.TIMEOUT);
|
||||
assert.equal(run.errorMessage, "Skill execution timed out");
|
||||
|
||||
const row = core
|
||||
.getDbInstance()
|
||||
.prepare("SELECT status FROM skill_executions WHERE id = ?")
|
||||
.get(run.id) as { status: string };
|
||||
assert.equal(row.status, "timeout");
|
||||
});
|
||||
|
||||
test("a handler that settles in time leaves no pending timeout timer behind", async () => {
|
||||
skillExecutor.setTimeout(30_000);
|
||||
const name = await registerSkill("timeout-fast");
|
||||
const before = pendingTimers();
|
||||
const run = await skillExecutor.execute(name, {}, { apiKeyId: API_KEY_ID });
|
||||
assert.equal(run.status, SkillStatus.SUCCESS);
|
||||
assert.ok(
|
||||
pendingTimers() <= before,
|
||||
"the 30s race timer must be cleared once the handler settles"
|
||||
);
|
||||
});
|
||||
|
|
@ -242,7 +242,7 @@ test("skillExecutor records disabled skills and missing skills as direct failure
|
|||
assert.equal(skillExecutor.listExecutions("key-a").length, 0);
|
||||
});
|
||||
|
||||
test("skillExecutor turns handler errors and timeouts into error executions", async () => {
|
||||
test("skillExecutor records handler errors as error and timeouts as timeout", async () => {
|
||||
await registerEchoSkill();
|
||||
|
||||
skillExecutor.registerHandler("echo-handler", async () => {
|
||||
|
|
@ -289,7 +289,7 @@ test("skillExecutor turns handler errors and timeouts into error executions", as
|
|||
);
|
||||
|
||||
assert.equal(skillExecutor["maxRetries"], 7);
|
||||
assert.equal(timedOut.status, "error");
|
||||
assert.equal(timedOut.status, "timeout");
|
||||
assert.equal(timedOut.output, null);
|
||||
assert.match(timedOut.errorMessage, /timed out/i);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue