@arhen/pi-core-subagent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +116 -0
- package/package.json +45 -0
- package/src/agents.ts +70 -0
- package/src/child.ts +153 -0
- package/src/index.ts +1173 -0
- package/src/mailbox.ts +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 minimalist-subagents contributors
|
|
4
|
+
Forked from @ghoulm370/pi-subagent-ui (MIT, Copyright (c) 2026 ghoulm370)
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# @arhen/pi-subagent
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@arhen/pi-subagent)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](https://github.com/earendil-works/pi)
|
|
6
|
+
|
|
7
|
+
Minimalist pi extension: **fast in-process subagents** with single / parallel / chain modes, background runs, cancellation, intercom (child↔leader) and an agent↔agent mailbox.
|
|
8
|
+
|
|
9
|
+
Built for one job: delegate work to isolated subagents **without bloating the parent context**.
|
|
10
|
+
|
|
11
|
+
## Design principles
|
|
12
|
+
|
|
13
|
+
- **No agent files required, no discovery overhead.** The leader defines every subagent inline per call — name, system prompt, toolset. Existing files (`.agents/`, `.pi/agents/`, `~/.pi/agent/agents/`) are *looked up by name only when no inline prompt is given*; nothing is ever created or scanned per request.
|
|
14
|
+
- **Two toolsets only.** Read-only (`read, grep, find, ls` — default) or write (`read, grep, find, ls, bash, edit, write` — `write: true`). No per-agent tool config surface.
|
|
15
|
+
- **In-process** — children are `AgentSession`s in the same runtime. No process spawn, no context bleed.
|
|
16
|
+
- **Zero parent-context injection.** No catalog, no context hook. 6 slim tools total.
|
|
17
|
+
- **Throttled updates** — widget/stream updates coalesce to ~6/s; no per-event deep clones.
|
|
18
|
+
- **No silent hangs** — watchdog aborts children that produce no events for 90s; per-task timeout 10min.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
pi install npm:@arhen/pi-subagent
|
|
24
|
+
# or locally: pi install /path/to/pi-subagents
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage — the leader invents the agents
|
|
28
|
+
|
|
29
|
+
Define agents inline (never creates files). If a task has **no inline `prompt`**, the extension looks up an existing file by agent name — `.agents/<name>.md` or `.pi/agents/<name>.md` (nearest project dir), then `~/.pi/agent/agents/<name>.md` — and inherits its prompt + tools. Inline params always win over the file. Model resolution: explicit `provider/model-id` (or bare id) via the pi model registry → agent-file `model` → the parent's current model → settings default.
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"agent": "api-reviewer",
|
|
34
|
+
"prompt": "You are a strict API reviewer. Check auth, rate limiting, and error handling. Cite file:line.",
|
|
35
|
+
"task": "Review src/api/upload.ts"
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Parallel — mixed toolsets, siblings can talk via mailbox:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"allowIntercom": true,
|
|
44
|
+
"tasks": [
|
|
45
|
+
{ "agent": "researcher", "prompt": "You find facts. Cite paths.", "task": "Map the auth flow", "write": false },
|
|
46
|
+
{ "agent": "implementer", "prompt": "You make minimal changes.", "task": "Implement POST /api/upload", "write": true }
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Chain — `{previous}` is replaced with the prior agent's output:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"chain": [
|
|
56
|
+
{ "agent": "planner", "prompt": "You write a step list.", "task": "Plan the change", "write": false },
|
|
57
|
+
{ "agent": "doer", "prompt": "You follow the plan exactly.", "task": "Execute: {previous}", "write": true }
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Background + intercom:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"agent": "auditor",
|
|
67
|
+
"prompt": "You audit dependencies.",
|
|
68
|
+
"task": "Audit package.json for outdated deps",
|
|
69
|
+
"background": true,
|
|
70
|
+
"allowIntercom": true
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Tools
|
|
75
|
+
|
|
76
|
+
| Tool | Purpose |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `subagent` | single / `tasks` (parallel) / `chain` (`{previous}`); `background:true` fire-and-forget; `allowIntercom:true` enables child talk tools; `notifyPerTask: true` wakes you as each task completes (default off) |
|
|
79
|
+
| `subagent_status` | live per-task snapshot (non-blocking) |
|
|
80
|
+
| `subagent_result` | full output of a run or one task |
|
|
81
|
+
| `await_subagent` | block until a run finishes (optional `timeoutMs`) |
|
|
82
|
+
| `reply_subagent` | answer a child's `ask_parent` question |
|
|
83
|
+
| `subagent_cancel` | abort a running/queued run |
|
|
84
|
+
|
|
85
|
+
### Per-task fields
|
|
86
|
+
|
|
87
|
+
`agent` (name you invent, or an existing file's name — required), `task` (required), `prompt` (system prompt, optional — falls back to file lookup, then a minimal default), `write` (toolset, default read-only), plus optional `model` (`provider/model-id`), `thinking` (validated enum: `off|minimal|low|medium|high|xhigh|max`), `tools` (explicit allowlist, overrides everything), `maxRuntimeMs`, `id`. Top-level only: `background`, `notifyPerTask`, `allowIntercom`, `concurrency`.
|
|
88
|
+
|
|
89
|
+
### Child talk tools (when `allowIntercom: true`)
|
|
90
|
+
|
|
91
|
+
| Tool | Meaning |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `ask_parent` | blocking question to the leader; parent answers via `reply_subagent` |
|
|
94
|
+
| `notify_parent` | one-way message to the leader |
|
|
95
|
+
| `send_agent_message` | message to a sibling subagent's mailbox (`to` = its task id, or `"leader"`) |
|
|
96
|
+
| `poll_agent_messages` | drain this subagent's mailbox |
|
|
97
|
+
|
|
98
|
+
## Context budget
|
|
99
|
+
|
|
100
|
+
- Parent tools: 6 schemas with short descriptions. **No catalog, no context hook** — nothing injected per request.
|
|
101
|
+
- Background completion: 3-line notice. Full text only via `subagent_result`.
|
|
102
|
+
- Children: isolated sessions; talk tools injected only when `allowIntercom`; each child's prompt states its own task id and its siblings' so mailbox addressing works.
|
|
103
|
+
|
|
104
|
+
## Development
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
bun install # dev deps (typecheck/test only; runtime uses pi's bundled SDK)
|
|
108
|
+
npx tsc --noEmit
|
|
109
|
+
bun test # pure-logic smoke tests (mailbox, failure classification, watchdog)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Runtime state: runs persist to `<parent-session>.subagents.json` sidecar; restored (non-terminal → aborted) on session start.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT.
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arhen/pi-core-subagent",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "pi extension: fast in-process subagents with single/parallel/chain, background runs, intercom and agent-to-agent mailbox. Leader defines agents inline.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "./src/index.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"src",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"pi",
|
|
15
|
+
"pi-coding-agent",
|
|
16
|
+
"pi-extension",
|
|
17
|
+
"pi-package",
|
|
18
|
+
"subagent",
|
|
19
|
+
"agent-orchestration"
|
|
20
|
+
],
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@earendil-works/pi-ai": "^0.84.2",
|
|
23
|
+
"@earendil-works/pi-agent-core": "^0.84.2",
|
|
24
|
+
"@earendil-works/pi-coding-agent": "^0.84.2",
|
|
25
|
+
"@earendil-works/pi-tui": "^0.84.2",
|
|
26
|
+
"typebox": "^1.3.14"
|
|
27
|
+
},
|
|
28
|
+
"pi": {
|
|
29
|
+
"extensions": [
|
|
30
|
+
"./src/index.ts"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/bun": "^1.3.14",
|
|
35
|
+
"typescript": "^7.0.2"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/arhen/pi-subagent.git"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public",
|
|
43
|
+
"registry": "https://registry.npmjs.org/"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/agents.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional agent-file lookup — fallback for inline definitions.
|
|
3
|
+
*
|
|
4
|
+
* The leader defines agents inline (prompt + toolset). If a task has no inline
|
|
5
|
+
* prompt, we LOOK UP a file by agent name — never create one:
|
|
6
|
+
* <cwd>/.agents/<name>.md (nearest project dir first)
|
|
7
|
+
* <cwd>/.pi/agents/<name>.md
|
|
8
|
+
* ~/.pi/agent/agents/<name>.md (user)
|
|
9
|
+
* First match wins; explicit inline params always override file contents.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
|
|
16
|
+
export interface FileAgent {
|
|
17
|
+
prompt: string;
|
|
18
|
+
tools?: string[];
|
|
19
|
+
model?: string;
|
|
20
|
+
thinking?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function projectAgentDirs(cwd: string): string[] {
|
|
24
|
+
const out: string[] = [];
|
|
25
|
+
let dir = cwd;
|
|
26
|
+
for (;;) {
|
|
27
|
+
for (const sub of [".agents", path.join(".pi", "agents")]) {
|
|
28
|
+
const candidate = path.join(dir, sub);
|
|
29
|
+
try {
|
|
30
|
+
if (fs.statSync(candidate).isDirectory()) out.push(candidate);
|
|
31
|
+
} catch {
|
|
32
|
+
/* missing dir is fine */
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const parent = path.dirname(dir);
|
|
36
|
+
if (parent === dir) break;
|
|
37
|
+
dir = parent;
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function lookupAgent(name: string, cwd: string): FileAgent | undefined {
|
|
43
|
+
const dirs = [...projectAgentDirs(cwd), path.join(getAgentDir(), "agents")];
|
|
44
|
+
for (const dir of dirs) {
|
|
45
|
+
const filePath = path.join(dir, `${name}.md`);
|
|
46
|
+
let content: string;
|
|
47
|
+
try {
|
|
48
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
49
|
+
} catch {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
|
|
54
|
+
if (!frontmatter.name || frontmatter.name !== name) continue;
|
|
55
|
+
const tools = frontmatter.tools
|
|
56
|
+
?.split(",")
|
|
57
|
+
.map((t) => t.trim())
|
|
58
|
+
.filter(Boolean);
|
|
59
|
+
return {
|
|
60
|
+
prompt: body,
|
|
61
|
+
tools: tools && tools.length > 0 ? tools : undefined,
|
|
62
|
+
model: frontmatter.model,
|
|
63
|
+
thinking: frontmatter.thinking,
|
|
64
|
+
};
|
|
65
|
+
} catch {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
package/src/child.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Child-session tools + watchdog.
|
|
3
|
+
*
|
|
4
|
+
* When `allowIntercom: true`, children get four talk tools:
|
|
5
|
+
* - ask_parent blocking Q&A with the leader (parent)
|
|
6
|
+
* - notify_parent one-way message to the leader
|
|
7
|
+
* - send_agent_message one-way message to another subagent's mailbox
|
|
8
|
+
* (`to` = target task id in this run, or "leader")
|
|
9
|
+
* - poll_agent_messages drain this subagent's mailbox
|
|
10
|
+
*
|
|
11
|
+
* Mailbox is a plain map in the manager; agents talk by polling, not push.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
15
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { Type } from "typebox";
|
|
17
|
+
|
|
18
|
+
export interface MailboxMessage {
|
|
19
|
+
from: string;
|
|
20
|
+
text: string;
|
|
21
|
+
at: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Child talk tools injected when allowIntercom: true. */
|
|
25
|
+
export const CHILD_TALK_TOOLS = ["ask_parent", "notify_parent", "send_agent_message", "poll_agent_messages"] as const;
|
|
26
|
+
|
|
27
|
+
export interface ChildHandlers {
|
|
28
|
+
onAskParent(taskId: string, question: string): Promise<string>;
|
|
29
|
+
onNotifyParent(taskId: string, message: string, level: "info" | "warning" | "error"): void;
|
|
30
|
+
/** Route a child→child message. Returns false when the target is unknown. */
|
|
31
|
+
onSendMessage(taskId: string, to: string, text: string): boolean;
|
|
32
|
+
/** Drain the mailbox (returns and clears pending messages). */
|
|
33
|
+
onPollMailbox(taskId: string): MailboxMessage[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createChildTools(taskId: string, handlers: ChildHandlers): ToolDefinition[] {
|
|
37
|
+
return [
|
|
38
|
+
{
|
|
39
|
+
name: "ask_parent",
|
|
40
|
+
label: "Ask Parent",
|
|
41
|
+
description:
|
|
42
|
+
"Ask the parent agent a clarifying question and BLOCK until it replies. Use sparingly — only when you truly cannot proceed without information only the parent has. Prefer figuring it out yourself.",
|
|
43
|
+
promptSnippet: "Ask the parent agent a question when truly blocked.",
|
|
44
|
+
promptGuidelines: [
|
|
45
|
+
"Use ask_parent only as a last resort when blocked on information only the parent has.",
|
|
46
|
+
"Ask one focused question at a time. The parent's reply resumes your work.",
|
|
47
|
+
],
|
|
48
|
+
parameters: Type.Object({
|
|
49
|
+
question: Type.String({ description: "A single, focused question for the parent agent" }),
|
|
50
|
+
}),
|
|
51
|
+
async execute(_toolCallId, params) {
|
|
52
|
+
const { question } = params as { question: string };
|
|
53
|
+
const answer = await handlers.onAskParent(taskId, question);
|
|
54
|
+
return { content: [{ type: "text" as const, text: answer || "(parent gave no answer)" }], details: {} };
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "notify_parent",
|
|
59
|
+
label: "Notify Parent",
|
|
60
|
+
description: "Send a non-blocking message to the parent agent (a finding, a risk, a heads-up). Your run continues immediately; the parent sees it on its next turn.",
|
|
61
|
+
promptSnippet: "Send the parent a non-blocking update or finding.",
|
|
62
|
+
parameters: Type.Object({
|
|
63
|
+
message: Type.String({ description: "The message content for the parent" }),
|
|
64
|
+
level: Type.Optional(StringEnum(["info", "warning", "error"] as const, { default: "info" })),
|
|
65
|
+
}),
|
|
66
|
+
async execute(_toolCallId, params) {
|
|
67
|
+
const { message, level } = params as { message: string; level?: "info" | "warning" | "error" };
|
|
68
|
+
handlers.onNotifyParent(taskId, message, level ?? "info");
|
|
69
|
+
return { content: [{ type: "text" as const, text: "Sent." }], details: {} };
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "send_agent_message",
|
|
74
|
+
label: "Send Agent Message",
|
|
75
|
+
description:
|
|
76
|
+
"Send a non-blocking message to another subagent in this run (delivered to its mailbox; it will see it via poll_agent_messages). Use 'leader' to message the parent instead. Messages are small and bounded — no long transcripts.",
|
|
77
|
+
promptSnippet: "Send a short message to a sibling subagent or the leader.",
|
|
78
|
+
parameters: Type.Object({
|
|
79
|
+
to: Type.String({ description: "Target task id of another subagent in this run (e.g. task_2), or 'leader' for the parent agent" }),
|
|
80
|
+
message: Type.String({ description: "Short message content (keep under ~500 chars)" }),
|
|
81
|
+
}),
|
|
82
|
+
async execute(_toolCallId, params) {
|
|
83
|
+
const { to, message } = params as { to: string; message: string };
|
|
84
|
+
if (!handlers.onSendMessage(taskId, to, message)) {
|
|
85
|
+
return { content: [{ type: "text" as const, text: `Unknown target '${to}'. Use a sibling task id in this run or 'leader'.` }], isError: true, details: {} };
|
|
86
|
+
}
|
|
87
|
+
return { content: [{ type: "text" as const, text: "Sent." }], details: {} };
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "poll_agent_messages",
|
|
92
|
+
label: "Poll Agent Messages",
|
|
93
|
+
description: "Check your mailbox for messages from sibling subagents. Returns and clears all pending messages. Call it before acting on assumptions about other agents' results.",
|
|
94
|
+
promptSnippet: "Check for messages from other subagents.",
|
|
95
|
+
parameters: Type.Object({}),
|
|
96
|
+
async execute() {
|
|
97
|
+
const messages = handlers.onPollMailbox(taskId);
|
|
98
|
+
if (messages.length === 0) return { content: [{ type: "text" as const, text: "No messages." }], details: {} };
|
|
99
|
+
const body = messages
|
|
100
|
+
.map((m) => `from ${m.from}: ${m.text}`)
|
|
101
|
+
.join("\n")
|
|
102
|
+
.slice(0, 4000);
|
|
103
|
+
return { content: [{ type: "text" as const, text: body }], details: { messages } };
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Watchdog — a subagent is "stalled" when it produces no events for stallMs.
|
|
111
|
+
* In-process AgentSessions have no process-exit signal, so we synthesize one
|
|
112
|
+
* via event-heartbeat. Every relevant child event must call touch().
|
|
113
|
+
*/
|
|
114
|
+
export interface Watchdog {
|
|
115
|
+
touch(): void;
|
|
116
|
+
readonly promise: Promise<never>;
|
|
117
|
+
dispose(): void;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function createWatchdog(stallMs: number, label: string): Watchdog {
|
|
121
|
+
let lastEventAt = Date.now();
|
|
122
|
+
let disposed = false;
|
|
123
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
124
|
+
|
|
125
|
+
const promise = new Promise<never>((_, reject) => {
|
|
126
|
+
const interval = Math.max(1000, Math.min(5000, Math.floor(stallMs / 4)));
|
|
127
|
+
timer = setInterval(() => {
|
|
128
|
+
if (disposed) return;
|
|
129
|
+
if (Date.now() - lastEventAt > stallMs) {
|
|
130
|
+
disposed = true;
|
|
131
|
+
if (timer) clearInterval(timer);
|
|
132
|
+
timer = undefined;
|
|
133
|
+
reject(new Error(`${label} stalled: no activity for ${Math.round(stallMs / 1000)}s`));
|
|
134
|
+
}
|
|
135
|
+
}, interval);
|
|
136
|
+
timer.unref?.();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
touch() {
|
|
141
|
+
lastEventAt = Date.now();
|
|
142
|
+
},
|
|
143
|
+
promise,
|
|
144
|
+
dispose() {
|
|
145
|
+
if (disposed) return;
|
|
146
|
+
disposed = true;
|
|
147
|
+
if (timer) {
|
|
148
|
+
clearInterval(timer);
|
|
149
|
+
timer = undefined;
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|