@rse/ase 1.0.7 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dst/ase-agent-definition-api.d.ts +184 -0
- package/dst/ase-agent-definition-api.js +6 -0
- package/dst/ase-agent-definition-builtin/condenser.js +42 -0
- package/dst/ase-agent-definition-builtin/editor.js +45 -0
- package/dst/ase-agent-definition-builtin/estimator.js +98 -0
- package/dst/ase-agent-definition-builtin/implementer.js +121 -0
- package/dst/ase-agent-definition-builtin/integrator.js +56 -0
- package/dst/ase-agent-definition-builtin/qualifier.js +61 -0
- package/dst/ase-agent-definition-builtin/rebooter.js +41 -0
- package/dst/ase-agent-definition-builtin/researcher.js +77 -0
- package/dst/ase-agent-definition-core.js +827 -0
- package/dst/ase-agent-fleet.js +772 -0
- package/dst/ase-agent-process.js +232 -0
- package/dst/ase-agent-relay-client.js +509 -0
- package/dst/ase-agent-relay-core.js +213 -0
- package/dst/ase-agent-relay-server-bind.js +242 -0
- package/dst/ase-agent-relay-server-cli.js +50 -0
- package/dst/ase-agent-runtime-claim.js +189 -0
- package/dst/ase-agent-runtime-engine.js +1100 -0
- package/dst/ase-agent-runtime-exec.js +275 -0
- package/dst/ase-agent-runtime-phase.js +581 -0
- package/dst/ase-agent-runtime-request.js +272 -0
- package/dst/ase-agent-runtime-tool.js +207 -0
- package/dst/ase-agent-runtime.js +1316 -0
- package/dst/ase-agent.js +65 -0
- package/dst/ase-artifact.js +6 -5
- package/dst/ase-config-core.js +465 -0
- package/dst/ase-config-schema.js +357 -0
- package/dst/ase-config-scope.js +172 -0
- package/dst/ase-config.js +25 -585
- package/dst/ase-hook.js +228 -111
- package/dst/ase-lib-daemon-cli.js +130 -0
- package/dst/ase-lib-hash.js +20 -0
- package/dst/{ase-log.js → ase-lib-log.js} +40 -7
- package/dst/ase-lib-server-bind.js +115 -0
- package/dst/ase-lib-server-cli.js +474 -0
- package/dst/{ase-table.js → ase-lib-table.js} +1 -1
- package/dst/{ase-version.js → ase-lib-version.js} +8 -1
- package/dst/ase-man.js +96 -0
- package/dst/ase-mcp.js +166 -13
- package/dst/{ase-getopt.js → ase-service-getopt.js} +82 -80
- package/dst/{ase-markdown.js → ase-service-markdown.js} +18 -11
- package/dst/ase-service-otel.js +1246 -0
- package/dst/{ase-skills.js → ase-service-skills.js} +29 -64
- package/dst/ase-service-state.js +101 -0
- package/dst/ase-service.js +269 -81
- package/dst/ase-setup-common.js +141 -0
- package/dst/ase-setup-mcp.js +359 -0
- package/dst/ase-setup-settings.js +837 -0
- package/dst/ase-setup.js +113 -869
- package/dst/ase-spec.js +31 -19
- package/dst/ase-statusline.js +108 -43
- package/dst/ase-task-board-core.js +739 -0
- package/dst/ase-task-board-filter.js +127 -0
- package/dst/ase-task-board-flow.js +237 -0
- package/dst/ase-task-board-graph.js +497 -0
- package/dst/ase-task-board-stats.js +263 -0
- package/dst/ase-task-board-tui-control.js +775 -0
- package/dst/ase-task-board-tui-model.js +1008 -0
- package/dst/ase-task-board-tui-popup.js +626 -0
- package/dst/ase-task-board-tui-style.js +106 -0
- package/dst/ase-task-board-tui-view.js +847 -0
- package/dst/ase-task-board-tui.js +189 -0
- package/dst/ase-task-board-web-client.css +1 -0
- package/dst/ase-task-board-web-client.html +20 -0
- package/dst/ase-task-board-web-client.js +81 -0
- package/dst/ase-task-board-web-server.js +823 -0
- package/dst/ase-task-board.js +134 -0
- package/dst/ase-task-format.js +757 -0
- package/dst/ase-task-request.js +208 -0
- package/dst/ase-task-store-client.js +495 -0
- package/dst/ase-task-store-core.js +804 -0
- package/dst/ase-task-store-issues.js +237 -0
- package/dst/ase-task-store-plugin-api.js +6 -0
- package/dst/ase-task-store-plugin-ase.js +484 -0
- package/dst/ase-task-store-plugin-delegate.js +137 -0
- package/dst/ase-task-store-plugin-gitea.js +666 -0
- package/dst/ase-task-store-plugin-github.js +696 -0
- package/dst/ase-task-store-plugin-gitlab.js +668 -0
- package/dst/ase-task-store-server-bind.js +538 -0
- package/dst/ase-task-store-server-cli.js +154 -0
- package/dst/ase-task.js +1335 -357
- package/dst/{ase-compat.js → ase-util-compat.js} +2 -2
- package/dst/{ase-diagram.js → ase-util-diagram.js} +107 -32
- package/dst/{ase-meta.js → ase-util-meta.js} +2 -2
- package/dst/{ase-metric.js → ase-util-metric.js} +2 -2
- package/dst/{ase-mint.js → ase-util-mint.js} +19 -12
- package/dst/{ase-worktree.js → ase-util-worktree.js} +30 -19
- package/dst/ase-util.js +20 -0
- package/dst/ase.js +108 -32
- package/dst/man/ase-agent-adhoc.1 +515 -0
- package/dst/man/ase-agent-condenser.1 +193 -0
- package/dst/man/ase-agent-editor.1 +212 -0
- package/dst/man/ase-agent-estimator.1 +214 -0
- package/dst/man/ase-agent-fleet.1 +185 -0
- package/dst/man/ase-agent-implementer.1 +233 -0
- package/dst/man/ase-agent-integrator.1 +213 -0
- package/dst/man/ase-agent-qualifier.1 +214 -0
- package/dst/man/ase-agent-rebooter.1 +193 -0
- package/dst/man/ase-agent-relay.1 +128 -0
- package/dst/man/ase-agent-researcher.1 +217 -0
- package/dst/man/ase-agent.1 +264 -0
- package/dst/man/ase-artifact-list.1 +38 -0
- package/dst/man/ase-artifact-name.1 +39 -0
- package/dst/man/ase-artifact.1 +56 -0
- package/dst/man/ase-config-delete.1 +45 -0
- package/dst/man/ase-config-edit.1 +48 -0
- package/dst/man/ase-config-get.1 +46 -0
- package/dst/man/ase-config-init.1 +60 -0
- package/dst/man/ase-config-list.1 +42 -0
- package/dst/man/ase-config-set.1 +52 -0
- package/dst/man/ase-config.1 +231 -0
- package/dst/man/ase-hook-permission-request.1 +36 -0
- package/dst/man/ase-hook-pre-tool-use.1 +59 -0
- package/dst/man/ase-hook-session-end.1 +47 -0
- package/dst/man/ase-hook-session-persona.1 +37 -0
- package/dst/man/ase-hook-session-start.1 +81 -0
- package/dst/man/ase-hook-stop.1 +34 -0
- package/dst/man/ase-hook-user-prompt-submit.1 +34 -0
- package/dst/man/ase-hook.1 +51 -0
- package/dst/man/ase-man.1 +67 -0
- package/dst/man/ase-mcp.1 +61 -0
- package/dst/man/ase-service-log.1 +43 -0
- package/dst/man/ase-service-send.1 +47 -0
- package/dst/man/ase-service-start.1 +54 -0
- package/dst/man/ase-service-status.1 +46 -0
- package/dst/man/ase-service-stop.1 +37 -0
- package/dst/man/ase-service.1 +85 -0
- package/dst/man/ase-setup-disable.1 +44 -0
- package/dst/man/ase-setup-enable.1 +45 -0
- package/dst/man/ase-setup-install.1 +73 -0
- package/dst/man/ase-setup-mcp.1 +124 -0
- package/dst/man/ase-setup-status.1 +60 -0
- package/dst/man/ase-setup-statusline.1 +92 -0
- package/dst/man/ase-setup-telemetry.1 +92 -0
- package/dst/man/ase-setup-uninstall.1 +59 -0
- package/dst/man/ase-setup-update.1 +64 -0
- package/dst/man/ase-setup.1 +93 -0
- package/dst/man/ase-spec-export.1 +62 -0
- package/dst/man/ase-spec-lint.1 +40 -0
- package/dst/man/ase-spec-preview.1 +40 -0
- package/dst/man/ase-spec.1 +46 -0
- package/dst/man/ase-statusline.1 +189 -0
- package/dst/man/ase-task-assign.1 +71 -0
- package/dst/man/ase-task-board.1 +384 -0
- package/dst/man/ase-task-delete.1 +35 -0
- package/dst/man/ase-task-edit.1 +46 -0
- package/dst/man/ase-task-idscheme.1 +65 -0
- package/dst/man/ase-task-lifecycle.1 +57 -0
- package/dst/man/ase-task-list.1 +47 -0
- package/dst/man/ase-task-load.1 +36 -0
- package/dst/man/ase-task-newid.1 +61 -0
- package/dst/man/ase-task-purge.1 +37 -0
- package/dst/man/ase-task-rename.1 +44 -0
- package/dst/man/ase-task-request.1 +61 -0
- package/dst/man/ase-task-save.1 +48 -0
- package/dst/man/ase-task-status.1 +55 -0
- package/dst/man/ase-task-store.1 +194 -0
- package/dst/man/ase-task-view.1 +40 -0
- package/dst/man/ase-task.1 +160 -0
- package/dst/man/ase-util-compat.1 +25 -0
- package/dst/man/ase-util-diagram.1 +76 -0
- package/dst/man/ase-util-meta.1 +45 -0
- package/dst/man/ase-util-metric.1 +54 -0
- package/dst/man/ase-util-mint.1 +59 -0
- package/dst/man/ase-util-worktree.1 +72 -0
- package/dst/man/ase-util.1 +50 -0
- package/dst/man/ase.1 +124 -0
- package/package.json +81 -21
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/agents/ase-meta-diagram.md +12 -8
- package/plugin/agents/ase-repo-review.md +322 -0
- package/plugin/hooks/hooks-codex.json +5 -0
- package/plugin/hooks/hooks-copilot.json +5 -0
- package/plugin/hooks/hooks.json +8 -0
- package/plugin/meta/ase-common-approach.md +117 -0
- package/plugin/meta/ase-common-changeset.md +256 -0
- package/plugin/meta/ase-common-code.md +10 -173
- package/plugin/meta/ase-common-grill.md +317 -0
- package/plugin/meta/ase-common-resolve.md +305 -0
- package/plugin/meta/ase-common-task.md +83 -4
- package/plugin/meta/ase-constitution.md +0 -2
- package/plugin/meta/ase-format-specbook.md +208 -25
- package/plugin/meta/ase-format-specbook.yaml +242 -851
- package/plugin/meta/ase-format-task.md +669 -125
- package/plugin/meta/ase-persona.md +3 -4
- package/plugin/meta/ase-skill.md +9 -0
- package/plugin/mods/ase-mods.ts +409 -0
- package/plugin/mods/claude-code.d.ts +13417 -0
- package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
- package/plugin/skills/ase-arch-analyze/help.md +2 -2
- package/plugin/skills/ase-code-analyze/SKILL.md +59 -8
- package/plugin/skills/ase-code-analyze/help.md +29 -4
- package/plugin/skills/ase-code-edit/SKILL.md +107 -181
- package/plugin/skills/ase-code-edit/help.md +118 -31
- package/plugin/skills/ase-code-lint/SKILL.md +64 -13
- package/plugin/skills/ase-code-lint/help.md +32 -2
- package/plugin/skills/ase-docs-proofread/SKILL.md +4 -4
- package/plugin/skills/ase-docs-proofread/help.md +1 -1
- package/plugin/skills/ase-docs-refine/SKILL.md +4 -4
- package/plugin/skills/ase-docs-shorten/SKILL.md +4 -4
- package/plugin/skills/ase-help-intent/data.md +2164 -1087
- package/plugin/skills/ase-help-intent/help.md +1 -1
- package/plugin/skills/ase-help-skill/catalog.md +12 -9
- package/plugin/skills/ase-meta-brainstorm/SKILL.md +1 -1
- package/plugin/skills/ase-meta-config/SKILL.md +3 -4
- package/plugin/skills/ase-meta-config/help.md +31 -10
- package/plugin/skills/ase-meta-mint/SKILL.md +5 -5
- package/plugin/skills/ase-meta-mint/help.md +10 -5
- package/plugin/skills/ase-meta-why/help.md +1 -1
- package/plugin/skills/ase-meta-workflow/SKILL.md +7 -7
- package/plugin/skills/ase-meta-workflow/help.md +4 -4
- package/plugin/skills/ase-meta-workflow/sample.md +3 -3
- package/plugin/skills/ase-meta-workflow/workflow.txt +22 -18
- package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/SKILL.md +4 -3
- package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/help.md +5 -5
- package/plugin/skills/{ase-meta-commit → ase-repo-commit}/SKILL.md +6 -6
- package/plugin/skills/{ase-meta-commit → ase-repo-commit}/help.md +5 -5
- package/plugin/skills/{ase-meta-diff → ase-repo-diff}/SKILL.md +6 -6
- package/plugin/skills/{ase-meta-diff → ase-repo-diff}/help.md +8 -8
- package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/SKILL.md +19 -19
- package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/help.md +10 -12
- package/plugin/skills/ase-repo-merge/SKILL.md +346 -0
- package/plugin/skills/ase-repo-merge/help.md +107 -0
- package/plugin/skills/ase-repo-resolve/SKILL.md +158 -0
- package/plugin/skills/ase-repo-resolve/help.md +102 -0
- package/plugin/skills/ase-repo-review/SKILL.md +406 -0
- package/plugin/skills/ase-repo-review/help.md +181 -0
- package/plugin/skills/ase-spec-edit/SKILL.md +117 -183
- package/plugin/skills/ase-spec-edit/help.md +107 -26
- package/plugin/skills/ase-sync-reconcile/help.md +1 -1
- package/plugin/skills/ase-task-assign/SKILL.md +111 -0
- package/plugin/skills/ase-task-assign/help.md +119 -0
- package/plugin/skills/ase-task-condense/SKILL.md +21 -21
- package/plugin/skills/ase-task-condense/help.md +12 -10
- package/plugin/skills/ase-task-delete/SKILL.md +1 -1
- package/plugin/skills/ase-task-dissect/SKILL.md +71 -48
- package/plugin/skills/ase-task-dissect/help.md +10 -9
- package/plugin/skills/ase-task-edit/SKILL.md +379 -94
- package/plugin/skills/ase-task-edit/help.md +95 -22
- package/plugin/skills/ase-task-estimate/SKILL.md +492 -0
- package/plugin/skills/ase-task-estimate/help.md +158 -0
- package/plugin/skills/ase-task-grill/SKILL.md +246 -138
- package/plugin/skills/ase-task-grill/help.md +129 -26
- package/plugin/skills/ase-task-id/SKILL.md +8 -1
- package/plugin/skills/ase-task-id/help.md +6 -3
- package/plugin/skills/ase-task-implement/SKILL.md +535 -145
- package/plugin/skills/ase-task-implement/help.md +138 -47
- package/plugin/skills/ase-task-integrate/SKILL.md +549 -0
- package/plugin/skills/ase-task-integrate/help.md +187 -0
- package/plugin/skills/ase-task-list/SKILL.md +55 -23
- package/plugin/skills/ase-task-list/help.md +45 -21
- package/plugin/skills/ase-task-reboot/SKILL.md +84 -43
- package/plugin/skills/ase-task-reboot/help.md +28 -8
- package/plugin/skills/ase-task-rename/SKILL.md +10 -2
- package/plugin/skills/ase-task-research/SKILL.md +386 -0
- package/plugin/skills/ase-task-research/help.md +102 -0
- package/plugin/skills/ase-task-status/SKILL.md +137 -0
- package/plugin/skills/ase-task-status/help.md +80 -0
- package/plugin/skills/ase-task-view/SKILL.md +49 -24
- package/plugin/skills/ase-task-view/help.md +19 -9
- package/plugin/agents/ase-meta-review.md +0 -187
- package/plugin/skills/ase-code-craft/SKILL.md +0 -264
- package/plugin/skills/ase-code-craft/help.md +0 -122
- package/plugin/skills/ase-code-refactor/SKILL.md +0 -232
- package/plugin/skills/ase-code-refactor/help.md +0 -109
- package/plugin/skills/ase-code-resolve/SKILL.md +0 -289
- package/plugin/skills/ase-code-resolve/help.md +0 -116
- package/plugin/skills/ase-meta-review/SKILL.md +0 -210
- package/plugin/skills/ase-meta-review/help.md +0 -96
- package/plugin/skills/ase-task-preflight/SKILL.md +0 -166
- package/plugin/skills/ase-task-preflight/help.md +0 -82
- /package/dst/{ase-ignore.js → ase-lib-ignore.js} +0 -0
- /package/dst/{ase-stdio.js → ase-lib-stdio.js} +0 -0
- /package/dst/{ase-kv.js → ase-service-kv.js} +0 -0
- /package/dst/{ase-sleep.js → ase-service-sleep.js} +0 -0
- /package/dst/{ase-timestamp.js → ase-service-timestamp.js} +0 -0
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Agentic Software Engineering (ASE)
|
|
3
|
+
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
+
*/
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import { ofetch } from "ofetch";
|
|
8
|
+
import { Agent } from "undici";
|
|
9
|
+
import * as v from "valibot";
|
|
10
|
+
import { WebSocket } from "ws";
|
|
11
|
+
import { isScalar } from "yaml";
|
|
12
|
+
import { Config } from "./ase-config-core.js";
|
|
13
|
+
import { configSchema } from "./ase-config-schema.js";
|
|
14
|
+
import { parseScope } from "./ase-config-scope.js";
|
|
15
|
+
import * as TaskFormat from "./ase-task-format.js";
|
|
16
|
+
import { urlHost, isLoopback } from "./ase-lib-server-bind.js";
|
|
17
|
+
import { storeSchema } from "./ase-task-store-server-cli.js";
|
|
18
|
+
import { relaySchema } from "./ase-agent-relay-server-cli.js";
|
|
19
|
+
import * as Relay from "./ase-agent-relay-core.js";
|
|
20
|
+
/* redact an agent relay URL for its use in messages: its user information, its
|
|
21
|
+
path (as it potentially embeds the token), and its query (unless it is the
|
|
22
|
+
well-known "?insecure"), so that no token ever lands in the logs */
|
|
23
|
+
export const redactURL = (url) => {
|
|
24
|
+
const m = /^([^:/?#]+:\/\/)?([^/?#]*)([^?#]*)(.*)$/s.exec(url);
|
|
25
|
+
if (m === null)
|
|
26
|
+
return "***";
|
|
27
|
+
const [, scheme = "", authority, path, rest] = m;
|
|
28
|
+
return scheme +
|
|
29
|
+
authority.replace(/^[^@]*@/, "***@") +
|
|
30
|
+
(path === "" || path === "/" ? path : "/***") +
|
|
31
|
+
(rest === "" || rest === "?insecure" ? rest : `${rest[0]}***`);
|
|
32
|
+
};
|
|
33
|
+
/* resolve the bearer token of the agent relay: the token embedded in the URL,
|
|
34
|
+
else $ASE_AGENT_RELAY_TOKEN, else "project.relay.token", else the token of the
|
|
35
|
+
locally started agent relay server (or of the locally started task store server
|
|
36
|
+
co-hosting the agent relay), but only if the URL addresses exactly this server;
|
|
37
|
+
a token potentially committed or redirected is warned about */
|
|
38
|
+
const relayToken = (log, relay, token, embedded, url) => {
|
|
39
|
+
const repoScoped = relay.scope === "project" || relay.scope.startsWith("task:");
|
|
40
|
+
if (embedded !== undefined) {
|
|
41
|
+
if (repoScoped)
|
|
42
|
+
log.write("warning", `agent: token embedded in "project.relay.url" on scope "${relay.scope}" ` +
|
|
43
|
+
"might be committed -- use $ASE_AGENT_RELAY_TOKEN or \"project.relay.token\" on scope \"user\" instead");
|
|
44
|
+
return embedded;
|
|
45
|
+
}
|
|
46
|
+
/* warn about a user token being sent to a non-loopback host selected on
|
|
47
|
+
a repository-supplied scope, as a cloned repository could redirect it this way */
|
|
48
|
+
const redirected = () => {
|
|
49
|
+
if (repoScoped && !isLoopback(url.hostname))
|
|
50
|
+
log.write("warning", `agent: sending token to non-loopback host "${url.host}" selected by ` +
|
|
51
|
+
`"project.relay.url" on scope "${relay.scope}" -- verify you trust this host` +
|
|
52
|
+
(url.protocol === "ase:" ? " (token is transmitted in plaintext)" : ""));
|
|
53
|
+
};
|
|
54
|
+
const env = process.env.ASE_AGENT_RELAY_TOKEN;
|
|
55
|
+
if (env !== undefined && env !== "") {
|
|
56
|
+
redirected();
|
|
57
|
+
return env;
|
|
58
|
+
}
|
|
59
|
+
if (token.value !== "") {
|
|
60
|
+
if (token.scope !== "user")
|
|
61
|
+
log.write("warning", `agent: "project.relay.token" found on scope "${token.scope}" ` +
|
|
62
|
+
"-- configure it on scope \"user\" only");
|
|
63
|
+
redirected();
|
|
64
|
+
return token.value;
|
|
65
|
+
}
|
|
66
|
+
/* fall back to the token of the locally started agent relay server, or of the
|
|
67
|
+
locally started task store server co-hosting it, but only if the URL addresses exactly this server */
|
|
68
|
+
const servers = [
|
|
69
|
+
{ name: "relay", cfg: new Config("relay", relaySchema, log, parseScope("user")) },
|
|
70
|
+
{ name: "store", cfg: new Config("store", storeSchema, log, parseScope("user")) }
|
|
71
|
+
];
|
|
72
|
+
for (const { name, cfg } of servers) {
|
|
73
|
+
cfg.read();
|
|
74
|
+
const scalar = (key) => {
|
|
75
|
+
const val = cfg.get(key);
|
|
76
|
+
return val === undefined ? "" : String(isScalar(val) ? val.value : val);
|
|
77
|
+
};
|
|
78
|
+
if (name === "store" && scalar("relay") !== "true")
|
|
79
|
+
continue;
|
|
80
|
+
const tok = scalar("token");
|
|
81
|
+
const address = scalar("address") || "127.0.0.1";
|
|
82
|
+
const wildcard = address === "0.0.0.0" || address === "::";
|
|
83
|
+
const host = urlHost(address).toLowerCase();
|
|
84
|
+
const loopback = isLoopback(url.hostname);
|
|
85
|
+
if (tok !== "" && scalar("port") === url.port
|
|
86
|
+
&& (url.hostname.toLowerCase() === host || (loopback && (wildcard || isLoopback(host)))))
|
|
87
|
+
return tok;
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`agent: no token for agent relay "${redactURL(relay.value)}" ` +
|
|
90
|
+
"(set $ASE_AGENT_RELAY_TOKEN, \"project.relay.token\" on scope \"user\", or \"/<token>\" in the URL)");
|
|
91
|
+
};
|
|
92
|
+
/* parse an agent relay URL "ase://<addr>:<port>[/<token>]" (selecting an agent relay
|
|
93
|
+
server via HTTP) or "ases://<addr>:<port>[/<token>][?insecure]" (selecting it via
|
|
94
|
+
HTTPS, optionally without certificate verification) */
|
|
95
|
+
export const parseRelayURL = (value) => {
|
|
96
|
+
/* parse via WHATWG URL, as it supports bracketed IPv6 addresses (e.g. "[::1]")
|
|
97
|
+
and retains the brackets in "hostname" for direct reuse in the HTTP URL */
|
|
98
|
+
const unsupported = () => new Error(`agent: unsupported agent relay URL "${redactURL(value)}" ` +
|
|
99
|
+
"(expected: \"ase://<addr>:<port>[/<token>]\" or \"ases://<addr>:<port>[/<token>][?insecure]\")");
|
|
100
|
+
const m = /^(ases?):\/\//.exec(value);
|
|
101
|
+
if (m === null)
|
|
102
|
+
throw unsupported();
|
|
103
|
+
const secure = m[1] === "ases";
|
|
104
|
+
let url;
|
|
105
|
+
try {
|
|
106
|
+
url = new URL(value);
|
|
107
|
+
}
|
|
108
|
+
catch (_err) {
|
|
109
|
+
throw unsupported();
|
|
110
|
+
}
|
|
111
|
+
let p;
|
|
112
|
+
if (url.hostname === "" || url.port === "" || url.username !== "" || url.password !== ""
|
|
113
|
+
|| url.hash !== "" || (url.search !== "" && !(secure && url.search === "?insecure"))
|
|
114
|
+
|| (p = /^(?:\/([^/]+))?$/.exec(url.pathname)) === null)
|
|
115
|
+
throw unsupported();
|
|
116
|
+
let embedded;
|
|
117
|
+
try {
|
|
118
|
+
embedded = p[1] !== undefined ? decodeURIComponent(p[1]) : undefined;
|
|
119
|
+
}
|
|
120
|
+
catch (_err) {
|
|
121
|
+
throw unsupported();
|
|
122
|
+
}
|
|
123
|
+
return { url, secure, embedded, insecure: url.search === "?insecure" };
|
|
124
|
+
};
|
|
125
|
+
/* resolve the agent relay of the project: the URL (see "parseRelayURL") is taken from
|
|
126
|
+
the given override (the "--agent-relay" option), else from "project.relay.url",
|
|
127
|
+
where no URL at all (null) disables the passing of the presence of the agents */
|
|
128
|
+
export const relaySpec = (log, override) => {
|
|
129
|
+
const cfg = new Config("config", configSchema, log);
|
|
130
|
+
cfg.read();
|
|
131
|
+
const scoped = (key) => {
|
|
132
|
+
const entry = cfg.getScoped(key);
|
|
133
|
+
if (entry === undefined)
|
|
134
|
+
return { value: "", scope: "" };
|
|
135
|
+
return {
|
|
136
|
+
value: String(isScalar(entry.value) ? entry.value.value : entry.value),
|
|
137
|
+
scope: Config.scopeLabel(entry.scope)
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
const relay = override !== undefined && override !== "" ? { value: override, scope: "option" } : scoped("project.relay.url");
|
|
141
|
+
if (relay.value === "")
|
|
142
|
+
return null;
|
|
143
|
+
const { url, secure, embedded, insecure } = parseRelayURL(relay.value);
|
|
144
|
+
/* require an explicit project id, as a basename-derived one
|
|
145
|
+
likely collides with unrelated projects on a shared server */
|
|
146
|
+
const prjId = scoped("project.id").value;
|
|
147
|
+
if (prjId === "")
|
|
148
|
+
throw new Error("agent: agent relay requires an explicit \"project.id\" " +
|
|
149
|
+
"(set it via \"ase config --scope project set project.id <id>\")");
|
|
150
|
+
if (!TaskFormat.ID_RE.test(prjId))
|
|
151
|
+
throw new Error(`agent: configured "project.id" "${prjId}" must match [A-Za-z0-9_-]+`);
|
|
152
|
+
return {
|
|
153
|
+
prjId,
|
|
154
|
+
base: `${secure ? "https" : "http"}://${url.hostname}:${url.port}`,
|
|
155
|
+
token: relayToken(log, relay, scoped("project.relay.token"), embedded, url),
|
|
156
|
+
insecure
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
/* an error of the agent relay: its HTTP status (or zero for an unreachable agent relay) */
|
|
160
|
+
export class AgentRelayError extends Error {
|
|
161
|
+
status;
|
|
162
|
+
constructor(message, status = 0, options) {
|
|
163
|
+
super(message, options);
|
|
164
|
+
this.status = status;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/* the interval of the keepalive pings of the subscription (where a connection without
|
|
168
|
+
any traffic for this interval is pinged and without any traffic for twice this interval
|
|
169
|
+
is terminated), the timeout of its WebSocket handshake, and the minimum and maximum
|
|
170
|
+
delay of its reconnection (doubled on each failed connection attempt in a row) */
|
|
171
|
+
const KEEPALIVE_MS = 15 * 1000;
|
|
172
|
+
const HANDSHAKE_MS = 10 * 1000;
|
|
173
|
+
const RECONNECT_MIN_MS = 2 * 1000;
|
|
174
|
+
const RECONNECT_MAX_MS = 60 * 1000;
|
|
175
|
+
/* the client of the agent relay: the announcing and removal of an agent,
|
|
176
|
+
the listing of the agents, and the subscription to their changes */
|
|
177
|
+
export class AgentRelayClient {
|
|
178
|
+
log;
|
|
179
|
+
spec;
|
|
180
|
+
dispatcher;
|
|
181
|
+
constructor(log, spec) {
|
|
182
|
+
this.log = log;
|
|
183
|
+
this.spec = spec;
|
|
184
|
+
this.dispatcher = spec.insecure ? new Agent({ connect: { rejectUnauthorized: false } }) : undefined;
|
|
185
|
+
}
|
|
186
|
+
/* the base URL of the agent relay */
|
|
187
|
+
get base() {
|
|
188
|
+
return this.spec.base;
|
|
189
|
+
}
|
|
190
|
+
/* perform a request, raising an error response or a failed connection */
|
|
191
|
+
async request(method, url, body) {
|
|
192
|
+
const r = await ofetch.raw(`${this.spec.base}/projects/${this.spec.prjId}/agents${url}`, {
|
|
193
|
+
method,
|
|
194
|
+
headers: {
|
|
195
|
+
Authorization: `Bearer ${this.spec.token}`,
|
|
196
|
+
...(body !== undefined ? { "Content-Type": "application/json" } : {})
|
|
197
|
+
},
|
|
198
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
199
|
+
dispatcher: this.dispatcher,
|
|
200
|
+
signal: AbortSignal.timeout(5000),
|
|
201
|
+
ignoreResponseError: true
|
|
202
|
+
}).catch((err) => {
|
|
203
|
+
let cause = err;
|
|
204
|
+
while (cause instanceof Error && cause.cause instanceof Error)
|
|
205
|
+
cause = cause.cause;
|
|
206
|
+
throw new AgentRelayError(`agent relay "${this.spec.base}" unreachable: ${cause instanceof Error ? cause.message : String(cause)}`, 0, { cause: err });
|
|
207
|
+
});
|
|
208
|
+
if (r.status < 200 || r.status >= 300) {
|
|
209
|
+
const d = r._data;
|
|
210
|
+
throw new AgentRelayError(`agent relay "${this.spec.base}": ` +
|
|
211
|
+
`${d?.detail ?? d?.title ?? "request failed"} (HTTP ${r.status})`, r.status);
|
|
212
|
+
}
|
|
213
|
+
return (r._data ?? {});
|
|
214
|
+
}
|
|
215
|
+
/* announce the presence of an agent, remove it, and list all agents */
|
|
216
|
+
async announce(id, presence) {
|
|
217
|
+
await this.request("PUT", `/${id}`, presence);
|
|
218
|
+
}
|
|
219
|
+
async remove(id) {
|
|
220
|
+
await this.request("DELETE", `/${id}`);
|
|
221
|
+
}
|
|
222
|
+
async list() {
|
|
223
|
+
return (await this.request("GET", "")).agents;
|
|
224
|
+
}
|
|
225
|
+
/* subscribe to the agents of the project via the WebSocket event endpoint,
|
|
226
|
+
reconnecting after a connection loss, a failed or timed out handshake, or
|
|
227
|
+
a connection without any traffic despite keepalive pings (e.g. a half-open one
|
|
228
|
+
after a network partition), with a growing delay while the connection attempts
|
|
229
|
+
keep failing, where a handshake rejected by the agent relay (e.g. for an invalid
|
|
230
|
+
token) is warned about once per reason, where the agents are unknown while disconnected,
|
|
231
|
+
reporting the agents on every change and each change of the connection state
|
|
232
|
+
(initially disconnected); returns a function to unsubscribe */
|
|
233
|
+
subscribe(onAgents, onState) {
|
|
234
|
+
const url = `${this.spec.base.replace(/^http/, "ws")}/projects/${this.spec.prjId}/agents/events`;
|
|
235
|
+
const agents = new Map();
|
|
236
|
+
let ws = null;
|
|
237
|
+
let timer = null;
|
|
238
|
+
let stopped = false;
|
|
239
|
+
let delay = RECONNECT_MIN_MS;
|
|
240
|
+
let rejected = "";
|
|
241
|
+
const connect = () => {
|
|
242
|
+
const sock = new WebSocket(url, {
|
|
243
|
+
headers: { Authorization: `Bearer ${this.spec.token}` },
|
|
244
|
+
rejectUnauthorized: !this.spec.insecure,
|
|
245
|
+
handshakeTimeout: HANDSHAKE_MS
|
|
246
|
+
});
|
|
247
|
+
ws = sock;
|
|
248
|
+
/* keep the connection alive: record any traffic (the frames, and the pings
|
|
249
|
+
of the agent relay and the pongs of our own pings), and terminate the
|
|
250
|
+
connection if there was none since the previous (then unanswered) ping */
|
|
251
|
+
let traffic = true;
|
|
252
|
+
let watchdog = null;
|
|
253
|
+
const alive = () => { traffic = true; };
|
|
254
|
+
sock.on("ping", alive);
|
|
255
|
+
sock.on("pong", alive);
|
|
256
|
+
sock.on("open", () => {
|
|
257
|
+
watchdog = setInterval(() => {
|
|
258
|
+
if (!traffic) {
|
|
259
|
+
this.log.write("debug", `agent relay "${this.spec.base}": events: connection unresponsive -- reconnecting`);
|
|
260
|
+
sock.terminate();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
traffic = false;
|
|
264
|
+
sock.ping();
|
|
265
|
+
}, KEEPALIVE_MS);
|
|
266
|
+
delay = RECONNECT_MIN_MS;
|
|
267
|
+
rejected = "";
|
|
268
|
+
onState?.(true);
|
|
269
|
+
});
|
|
270
|
+
/* apply the validated event frames to the agents, where an invalid
|
|
271
|
+
frame is warned about (once per connection) and ignored */
|
|
272
|
+
let warned = false;
|
|
273
|
+
sock.on("message", (data) => {
|
|
274
|
+
traffic = true;
|
|
275
|
+
try {
|
|
276
|
+
let frame;
|
|
277
|
+
try {
|
|
278
|
+
const result = v.safeParse(Relay.frameSchema, JSON.parse(String(data)));
|
|
279
|
+
if (!result.success)
|
|
280
|
+
throw new Error(`${result.issues[0].message}` +
|
|
281
|
+
(result.issues[0].path !== undefined ? ` (at "${v.getDotPath(result.issues[0]) ?? ""}")` : ""));
|
|
282
|
+
frame = result.output;
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
this.log.write(warned ? "debug" : "warning", `agent relay "${this.spec.base}": events: ` +
|
|
286
|
+
`invalid event frame ignored: ${err instanceof Error ? err.message : String(err)}`);
|
|
287
|
+
warned = true;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (frame.reset === true)
|
|
291
|
+
agents.clear();
|
|
292
|
+
for (const [id, agent] of Object.entries(frame.agents ?? {}))
|
|
293
|
+
agents.set(id, { ...agent, recent: Relay.appendLines(agents.get(id)?.recent ?? [], agent.recent) });
|
|
294
|
+
for (const id of frame.removed ?? [])
|
|
295
|
+
agents.delete(id);
|
|
296
|
+
onAgents([...agents.values()]);
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
/* never let a failure escape the listener (and crash the process) */
|
|
300
|
+
this.log.write("warning", `agent relay "${this.spec.base}": events: ` +
|
|
301
|
+
`failed to process event frame: ${err instanceof Error ? err.message : String(err)}`);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
sock.on("error", (err) => {
|
|
305
|
+
const rejection = /^Unexpected server response: 4\d\d$/.test(err.message);
|
|
306
|
+
this.log.write(rejection && err.message !== rejected ? "warning" : "debug", `agent relay "${this.spec.base}": events: ${err.message}`);
|
|
307
|
+
if (rejection)
|
|
308
|
+
rejected = err.message;
|
|
309
|
+
});
|
|
310
|
+
sock.on("close", () => {
|
|
311
|
+
if (watchdog !== null)
|
|
312
|
+
clearInterval(watchdog);
|
|
313
|
+
ws = null;
|
|
314
|
+
if (!stopped) {
|
|
315
|
+
if (agents.size > 0) {
|
|
316
|
+
agents.clear();
|
|
317
|
+
onAgents([]);
|
|
318
|
+
}
|
|
319
|
+
onState?.(false);
|
|
320
|
+
timer = setTimeout(connect, delay);
|
|
321
|
+
delay = Math.min(delay * 2, RECONNECT_MAX_MS);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
};
|
|
325
|
+
connect();
|
|
326
|
+
return () => {
|
|
327
|
+
stopped = true;
|
|
328
|
+
if (timer !== null)
|
|
329
|
+
clearTimeout(timer);
|
|
330
|
+
ws?.close();
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/* the interval of the heartbeat, the coalescing delay of the processing log lines,
|
|
335
|
+
the minimum and maximum back-off delay after a rejection, and the
|
|
336
|
+
maximum duration of the removal of the presence on stop */
|
|
337
|
+
const HEARTBEAT_MS = 10 * 1000;
|
|
338
|
+
const COALESCE_MS = 250;
|
|
339
|
+
const BACKOFF_MIN_MS = 60 * 1000;
|
|
340
|
+
const BACKOFF_MAX_MS = 15 * 60 * 1000;
|
|
341
|
+
const STOP_MS = 2 * 1000;
|
|
342
|
+
/* the user name and id of the agent process (with an unknown user id, e.g. on Windows, as -1) */
|
|
343
|
+
const processUser = () => {
|
|
344
|
+
try {
|
|
345
|
+
const info = os.userInfo();
|
|
346
|
+
return { name: info.username, uid: info.uid };
|
|
347
|
+
}
|
|
348
|
+
catch (_err) {
|
|
349
|
+
return { name: process.env.USER ?? process.env.USERNAME ?? "", uid: process.getuid?.() ?? -1 };
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
/* the presence of a running agent, announced to the agent relay: immediately
|
|
353
|
+
on every change of its state, tasks, and usage, coalesced on new processing log lines,
|
|
354
|
+
and periodically as its heartbeat, each time with only the processing log lines
|
|
355
|
+
added since the previous successful announcement (kept until announced, bounded
|
|
356
|
+
while the agent relay is unreachable), where an unreachable agent relay is
|
|
357
|
+
warned about once per outage only and never disturbs the agent itself, and where
|
|
358
|
+
a rejected announcement (a client error other than a timeout) backs off exponentially,
|
|
359
|
+
and a permanently rejected one (also other than an authentication failure or a
|
|
360
|
+
rate limit) additionally drops its lines */
|
|
361
|
+
export class AgentPresenceReporter {
|
|
362
|
+
log;
|
|
363
|
+
client;
|
|
364
|
+
kind;
|
|
365
|
+
name;
|
|
366
|
+
mode;
|
|
367
|
+
request;
|
|
368
|
+
prompt;
|
|
369
|
+
states;
|
|
370
|
+
title;
|
|
371
|
+
description;
|
|
372
|
+
id;
|
|
373
|
+
started = Date.now();
|
|
374
|
+
cwd = process.cwd();
|
|
375
|
+
user = processUser();
|
|
376
|
+
state = "waiting";
|
|
377
|
+
tasks = [];
|
|
378
|
+
usage = null;
|
|
379
|
+
lines = [];
|
|
380
|
+
beat = null;
|
|
381
|
+
delay = null;
|
|
382
|
+
flight = null;
|
|
383
|
+
again = false;
|
|
384
|
+
failing = false;
|
|
385
|
+
reason = "";
|
|
386
|
+
backoff = 0;
|
|
387
|
+
resume = 0;
|
|
388
|
+
stopped = false;
|
|
389
|
+
constructor(log, client, kind, name, mode = "", request = "", prompt = "required", states, title = "", description = "") {
|
|
390
|
+
this.log = log;
|
|
391
|
+
this.client = client;
|
|
392
|
+
this.kind = kind;
|
|
393
|
+
this.name = name;
|
|
394
|
+
this.mode = mode;
|
|
395
|
+
this.request = request;
|
|
396
|
+
this.prompt = prompt;
|
|
397
|
+
this.states = states;
|
|
398
|
+
this.title = title;
|
|
399
|
+
this.description = description;
|
|
400
|
+
this.id = `${os.hostname()}-${process.pid}-${name}`
|
|
401
|
+
.replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^[^A-Za-z0-9]+/, "").slice(0, 128);
|
|
402
|
+
}
|
|
403
|
+
/* start announcing the agent (initially waiting) */
|
|
404
|
+
start() {
|
|
405
|
+
this.beat = setInterval(() => { this.send(); }, HEARTBEAT_MS);
|
|
406
|
+
this.beat.unref();
|
|
407
|
+
this.send();
|
|
408
|
+
}
|
|
409
|
+
/* update the tasks the agent is busy with (and hence its state) */
|
|
410
|
+
busy(tasks) {
|
|
411
|
+
this.tasks = [...tasks].sort(TaskFormat.compareIds);
|
|
412
|
+
this.state = this.tasks.length > 0 ? "busy" : "waiting";
|
|
413
|
+
this.send();
|
|
414
|
+
}
|
|
415
|
+
/* record the usage of the agent tool by a finished job (as the last one, and into the total) */
|
|
416
|
+
used(usage) {
|
|
417
|
+
this.usage = { total: this.usage !== null ? Relay.addUsage(this.usage.total, usage) : usage, last: usage };
|
|
418
|
+
this.send();
|
|
419
|
+
}
|
|
420
|
+
/* record a processing log line (of a task, or of the agent itself) */
|
|
421
|
+
line(level, task, msg) {
|
|
422
|
+
this.lines.push({ time: Date.now(), level, task, msg: msg.length > Relay.MSG_MAX ? `${msg.slice(0, Relay.MSG_MAX - 1)}…` : msg });
|
|
423
|
+
if (this.lines.length > Relay.LINES_MAX)
|
|
424
|
+
this.lines.splice(0, this.lines.length - Relay.LINES_MAX);
|
|
425
|
+
if (this.delay === null && !this.stopped)
|
|
426
|
+
this.delay = setTimeout(() => {
|
|
427
|
+
this.delay = null;
|
|
428
|
+
this.send();
|
|
429
|
+
}, COALESCE_MS);
|
|
430
|
+
}
|
|
431
|
+
/* announce the current presence, serialized with an ongoing announcement,
|
|
432
|
+
but not while backing off after a rejection */
|
|
433
|
+
send() {
|
|
434
|
+
if (this.stopped || Date.now() < this.resume)
|
|
435
|
+
return;
|
|
436
|
+
if (this.flight !== null) {
|
|
437
|
+
this.again = true;
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const lines = this.lines;
|
|
441
|
+
this.lines = [];
|
|
442
|
+
const presence = {
|
|
443
|
+
kind: this.kind, name: this.name, mode: this.mode, host: os.hostname(), pid: process.pid,
|
|
444
|
+
cwd: this.cwd, user: this.user.name, uid: this.user.uid, started: this.started,
|
|
445
|
+
state: this.state, tasks: this.tasks, usage: this.usage, lines
|
|
446
|
+
};
|
|
447
|
+
if (this.title !== "")
|
|
448
|
+
presence.title = this.title;
|
|
449
|
+
if (this.description !== "")
|
|
450
|
+
presence.description = this.description;
|
|
451
|
+
if (this.request !== "")
|
|
452
|
+
presence.request = this.request;
|
|
453
|
+
if (this.request !== "" && this.prompt !== "required")
|
|
454
|
+
presence.prompt = this.prompt;
|
|
455
|
+
if (this.states !== undefined)
|
|
456
|
+
presence.states = this.states;
|
|
457
|
+
this.flight = this.client.announce(this.id, presence).then(() => {
|
|
458
|
+
if (this.failing)
|
|
459
|
+
this.log.write("info", `agent: agent relay "${this.client.base}" accepting presence of agent again`);
|
|
460
|
+
this.failing = false;
|
|
461
|
+
this.reason = "";
|
|
462
|
+
this.backoff = 0;
|
|
463
|
+
this.resume = 0;
|
|
464
|
+
}).catch((err) => {
|
|
465
|
+
const status = err instanceof AgentRelayError ? err.status : 0;
|
|
466
|
+
const rejected = status >= 400 && status < 500 && status !== 408;
|
|
467
|
+
const permanent = rejected && status !== 401 && status !== 429;
|
|
468
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
469
|
+
/* on a rejection (a client error other than a timeout), back off exponentially
|
|
470
|
+
(as a repetition is likely rejected again, e.g. by the limits of the agent relay
|
|
471
|
+
or for an invalid token), and on a permanent rejection (also other than an
|
|
472
|
+
authentication failure or a rate limit) drop the rejected lines (as they
|
|
473
|
+
potentially caused it), else keep the not announced lines (in front of the
|
|
474
|
+
meanwhile added ones) for the next announcement */
|
|
475
|
+
if (rejected) {
|
|
476
|
+
this.backoff = Math.min(Math.max(this.backoff * 2, BACKOFF_MIN_MS), BACKOFF_MAX_MS);
|
|
477
|
+
this.resume = Date.now() + this.backoff;
|
|
478
|
+
}
|
|
479
|
+
if (!permanent)
|
|
480
|
+
this.lines = [...lines, ...this.lines].slice(-Relay.LINES_MAX);
|
|
481
|
+
if (!this.failing || (rejected && message !== this.reason))
|
|
482
|
+
this.log.write("warning", `agent: ${message} ` + (rejected ?
|
|
483
|
+
`(presence of agent rejected -- retrying in ${Math.round(this.backoff / 1000)}s at the earliest)` :
|
|
484
|
+
"(presence of agent not passed until reachable again)"));
|
|
485
|
+
this.failing = true;
|
|
486
|
+
this.reason = message;
|
|
487
|
+
}).finally(() => {
|
|
488
|
+
this.flight = null;
|
|
489
|
+
if (this.again) {
|
|
490
|
+
this.again = false;
|
|
491
|
+
this.send();
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
/* stop announcing the agent and remove its presence, bounded in time, so an
|
|
496
|
+
unresponsive agent relay never delays the shutdown of the agent (a presence
|
|
497
|
+
not removed in time is swept by the agent relay as stale anyway) */
|
|
498
|
+
async stop() {
|
|
499
|
+
this.stopped = true;
|
|
500
|
+
if (this.beat !== null)
|
|
501
|
+
clearInterval(this.beat);
|
|
502
|
+
if (this.delay !== null)
|
|
503
|
+
clearTimeout(this.delay);
|
|
504
|
+
const remove = Promise.resolve(this.flight)
|
|
505
|
+
.then(() => this.client.remove(this.id))
|
|
506
|
+
.catch(() => { });
|
|
507
|
+
await Promise.race([remove, new Promise((resolve) => { setTimeout(resolve, STOP_MS).unref(); })]);
|
|
508
|
+
}
|
|
509
|
+
}
|