@tintinweb/pi-subagents 0.6.3 → 0.7.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.
@@ -6,7 +6,8 @@
6
6
  */
7
7
  import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
8
8
  import { extractText } from "../context.js";
9
- import { describeActivity, formatDuration, formatTokens, getDisplayName, getPromptModeLabel } from "./agent-widget.js";
9
+ import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
10
+ import { describeActivity, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel } from "./agent-widget.js";
10
11
  /** Lines consumed by chrome: top border + header + header sep + footer sep + footer + bottom border. */
11
12
  const CHROME_LINES = 6;
12
13
  const MIN_VIEWPORT = 3;
@@ -101,13 +102,10 @@ export class ConversationViewer {
101
102
  const toolUses = this.activity?.toolUses ?? this.record.toolUses;
102
103
  if (toolUses > 0)
103
104
  headerParts.unshift(`${toolUses} tool${toolUses === 1 ? "" : "s"}`);
104
- if (this.activity?.session) {
105
- try {
106
- const tokens = this.activity.session.getSessionStats().tokens.total;
107
- if (tokens > 0)
108
- headerParts.push(formatTokens(tokens));
109
- }
110
- catch { /* */ }
105
+ const tokens = getLifetimeTotal(this.activity?.lifetimeUsage);
106
+ if (tokens > 0) {
107
+ const percent = getSessionContextPercent(this.activity?.session);
108
+ headerParts.push(formatSessionTokens(tokens, percent, th, this.record.compactionCount));
111
109
  }
112
110
  lines.push(row(`${statusIcon} ${th.bold(name)}${modeTag} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${th.fg("dim", headerParts.join(" · "))}`));
113
111
  lines.push(hrMid);
@@ -0,0 +1,16 @@
1
+ /**
2
+ * schedule-menu.ts — `/agents → Scheduled jobs` submenu.
3
+ *
4
+ * Minimal v1 surface: list scheduled jobs, select one to inspect details +
5
+ * confirm cancellation. No create wizard (the `Agent` tool's `schedule` param
6
+ * is the canonical creation path), no toggle/cleanup (cancel is enough for
7
+ * "I scheduled something dumb, get rid of it"). Add management surfaces here
8
+ * if real demand emerges.
9
+ */
10
+ import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
11
+ import type { SubagentScheduler } from "../schedule.js";
12
+ /**
13
+ * List scheduled jobs; selecting one opens a cancel-confirm with details.
14
+ * Returns when the user backs out or after a cancellation.
15
+ */
16
+ export declare function showSchedulesMenu(ctx: ExtensionCommandContext, scheduler: SubagentScheduler): Promise<void>;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * schedule-menu.ts — `/agents → Scheduled jobs` submenu.
3
+ *
4
+ * Minimal v1 surface: list scheduled jobs, select one to inspect details +
5
+ * confirm cancellation. No create wizard (the `Agent` tool's `schedule` param
6
+ * is the canonical creation path), no toggle/cleanup (cancel is enough for
7
+ * "I scheduled something dumb, get rid of it"). Add management surfaces here
8
+ * if real demand emerges.
9
+ */
10
+ /** Format an ISO timestamp as relative time ("in 4h", "2d ago", "—"). */
11
+ function relTime(iso, now = Date.now()) {
12
+ if (!iso)
13
+ return "—";
14
+ const t = new Date(iso).getTime();
15
+ if (Number.isNaN(t))
16
+ return "—";
17
+ const diff = t - now;
18
+ const abs = Math.abs(diff);
19
+ const future = diff > 0;
20
+ if (abs < 60_000)
21
+ return future ? "in <1m" : "<1m ago";
22
+ const m = Math.round(abs / 60_000);
23
+ if (m < 60)
24
+ return future ? `in ${m}m` : `${m}m ago`;
25
+ const h = Math.round(abs / 3_600_000);
26
+ if (h < 24)
27
+ return future ? `in ${h}h` : `${h}h ago`;
28
+ const d = Math.round(abs / 86_400_000);
29
+ return future ? `in ${d}d` : `${d}d ago`;
30
+ }
31
+ /** One-line status icon. */
32
+ function statusIcon(j) {
33
+ if (!j.enabled)
34
+ return "✗";
35
+ if (j.lastStatus === "error")
36
+ return "!";
37
+ if (j.lastStatus === "running")
38
+ return "⋯";
39
+ return "✓";
40
+ }
41
+ /** Compact selectable row — name, schedule, agent type, next/last run, count. */
42
+ function formatJob(j, scheduler) {
43
+ const next = scheduler.getNextRun(j.id);
44
+ return [
45
+ statusIcon(j),
46
+ j.name.padEnd(18).slice(0, 18),
47
+ j.schedule.padEnd(14).slice(0, 14),
48
+ `[${j.subagent_type}]`,
49
+ `next ${relTime(next)}`,
50
+ `last ${relTime(j.lastRun)}`,
51
+ `runs ${j.runCount}`,
52
+ ].join(" ");
53
+ }
54
+ /** Multi-line details block for the cancel confirm. */
55
+ function formatDetails(j, scheduler) {
56
+ const next = scheduler.getNextRun(j.id) ?? "—";
57
+ return [
58
+ `name: ${j.name}`,
59
+ `schedule: ${j.schedule} (${j.scheduleType})`,
60
+ `agent: ${j.subagent_type}`,
61
+ `prompt: ${j.prompt.slice(0, 200)}${j.prompt.length > 200 ? "…" : ""}`,
62
+ `created: ${j.createdAt}`,
63
+ `last run: ${j.lastRun ?? "—"} (${j.lastStatus ?? "—"})`,
64
+ `next run: ${next}`,
65
+ `runs: ${j.runCount}`,
66
+ ].join("\n");
67
+ }
68
+ /**
69
+ * List scheduled jobs; selecting one opens a cancel-confirm with details.
70
+ * Returns when the user backs out or after a cancellation.
71
+ */
72
+ export async function showSchedulesMenu(ctx, scheduler) {
73
+ if (!scheduler.isActive()) {
74
+ ctx.ui.notify("Scheduler is not active in this session.", "warning");
75
+ return;
76
+ }
77
+ const jobs = scheduler.list();
78
+ if (jobs.length === 0) {
79
+ ctx.ui.notify("No scheduled jobs.", "info");
80
+ return;
81
+ }
82
+ const labels = jobs.map(j => formatJob(j, scheduler));
83
+ const choice = await ctx.ui.select(`Scheduled jobs (${jobs.length}) — select to cancel`, labels);
84
+ if (!choice)
85
+ return;
86
+ const idx = labels.indexOf(choice);
87
+ if (idx < 0)
88
+ return;
89
+ const job = jobs[idx];
90
+ const ok = await ctx.ui.confirm(`Cancel "${job.name}"?`, formatDetails(job, scheduler));
91
+ if (!ok)
92
+ return;
93
+ scheduler.removeJob(job.id);
94
+ ctx.ui.notify(`Cancelled "${job.name}".`, "info");
95
+ }
@@ -0,0 +1,50 @@
1
+ /** usage.ts — Token usage: shapes, accumulator operators, session-stats readers. */
2
+ /**
3
+ * Lifetime usage components, accumulated via `message_end` events. Survives
4
+ * compaction (which replaces session.state.messages and would reset any
5
+ * stats-derived sum). cacheRead is excluded because each turn's cacheRead is
6
+ * the cumulative cached prefix re-read on that one call — summing across
7
+ * turns counts the prefix N times. See issue #38.
8
+ */
9
+ export type LifetimeUsage = {
10
+ input: number;
11
+ output: number;
12
+ cacheWrite: number;
13
+ };
14
+ /** Sum of lifetime usage components, or 0 if undefined. */
15
+ export declare function getLifetimeTotal(u?: LifetimeUsage): number;
16
+ /** Add a usage delta into a target accumulator (mutates target). */
17
+ export declare function addUsage(into: LifetimeUsage, delta: LifetimeUsage): void;
18
+ /** Minimal shape we read from upstream `getSessionStats()`. */
19
+ export type SessionStatsLike = {
20
+ tokens: {
21
+ input: number;
22
+ output: number;
23
+ cacheWrite: number;
24
+ };
25
+ contextUsage?: {
26
+ percent: number | null;
27
+ };
28
+ };
29
+ export type SessionLike = {
30
+ getSessionStats(): SessionStatsLike;
31
+ };
32
+ /**
33
+ * Session-scoped token count: input + output + cacheWrite as reported by
34
+ * upstream `getSessionStats().tokens` for the *current* session window.
35
+ *
36
+ * RESETS at compaction — upstream replaces `session.state.messages` and the
37
+ * stats are derived from that array. For a lifetime total that survives
38
+ * compaction, use `getLifetimeTotal(lifetimeUsage)` instead, which reads
39
+ * from an independent accumulator fed by `message_end` events.
40
+ *
41
+ * Avoids upstream's `tokens.total` field, which sums per-turn `cacheRead`
42
+ * and so counts the cumulative cached prefix N times across N turns
43
+ * (issue #38).
44
+ */
45
+ export declare function getSessionTokens(session: SessionLike | undefined): number;
46
+ /**
47
+ * Context-window utilization (0–100), or null when unavailable
48
+ * (no model contextWindow, or post-compaction before the next response).
49
+ */
50
+ export declare function getSessionContextPercent(session: SessionLike | undefined): number | null;
package/dist/usage.js ADDED
@@ -0,0 +1,49 @@
1
+ /** usage.ts — Token usage: shapes, accumulator operators, session-stats readers. */
2
+ /** Sum of lifetime usage components, or 0 if undefined. */
3
+ export function getLifetimeTotal(u) {
4
+ return u ? u.input + u.output + u.cacheWrite : 0;
5
+ }
6
+ /** Add a usage delta into a target accumulator (mutates target). */
7
+ export function addUsage(into, delta) {
8
+ into.input += delta.input;
9
+ into.output += delta.output;
10
+ into.cacheWrite += delta.cacheWrite;
11
+ }
12
+ /**
13
+ * Session-scoped token count: input + output + cacheWrite as reported by
14
+ * upstream `getSessionStats().tokens` for the *current* session window.
15
+ *
16
+ * RESETS at compaction — upstream replaces `session.state.messages` and the
17
+ * stats are derived from that array. For a lifetime total that survives
18
+ * compaction, use `getLifetimeTotal(lifetimeUsage)` instead, which reads
19
+ * from an independent accumulator fed by `message_end` events.
20
+ *
21
+ * Avoids upstream's `tokens.total` field, which sums per-turn `cacheRead`
22
+ * and so counts the cumulative cached prefix N times across N turns
23
+ * (issue #38).
24
+ */
25
+ export function getSessionTokens(session) {
26
+ if (!session)
27
+ return 0;
28
+ try {
29
+ const t = session.getSessionStats().tokens;
30
+ return t.input + t.output + t.cacheWrite;
31
+ }
32
+ catch {
33
+ return 0;
34
+ }
35
+ }
36
+ /**
37
+ * Context-window utilization (0–100), or null when unavailable
38
+ * (no model contextWindow, or post-compaction before the next response).
39
+ */
40
+ export function getSessionContextPercent(session) {
41
+ if (!session)
42
+ return null;
43
+ try {
44
+ return session.getSessionStats().contextUsage?.percent ?? null;
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tintinweb/pi-subagents",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
4
4
  "description": "A pi extension extension that brings smart Claude Code-style autonomous sub-agents to pi.",
5
5
  "author": "tintinweb",
6
6
  "license": "MIT",
@@ -20,11 +20,15 @@
20
20
  "agent",
21
21
  "autonomous"
22
22
  ],
23
+ "peerDependencies": {
24
+ "@mariozechner/pi-ai": ">=0.70.5",
25
+ "@mariozechner/pi-coding-agent": ">=0.70.5",
26
+ "@mariozechner/pi-tui": ">=0.70.5"
27
+ },
23
28
  "dependencies": {
24
- "@mariozechner/pi-ai": "^0.70.5",
25
- "@mariozechner/pi-coding-agent": "^0.70.5",
26
- "@mariozechner/pi-tui": "^0.70.5",
27
- "@sinclair/typebox": "latest"
29
+ "@sinclair/typebox": "^0.34.49",
30
+ "croner": "^10.0.1",
31
+ "nanoid": "^5.0.0"
28
32
  },
29
33
  "scripts": {
30
34
  "build": "tsc",
@@ -36,7 +40,7 @@
36
40
  "lint:fix": "biome check --fix src/ test/"
37
41
  },
38
42
  "devDependencies": {
39
- "@biomejs/biome": "^2.3.5",
43
+ "@biomejs/biome": "^2.4.14",
40
44
  "@types/node": "^25.5.0",
41
45
  "typescript": "^6.0.0",
42
46
  "vitest": "^4.0.18"
@@ -11,10 +11,13 @@ import type { Model } from "@mariozechner/pi-ai";
11
11
  import type { AgentSession, ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
12
12
  import { resumeAgent, runAgent, type ToolActivity } from "./agent-runner.js";
13
13
  import type { AgentRecord, IsolationMode, SubagentType, ThinkingLevel } from "./types.js";
14
+ import { addUsage } from "./usage.js";
14
15
  import { cleanupWorktree, createWorktree, pruneWorktrees, } from "./worktree.js";
15
16
 
16
17
  export type OnAgentComplete = (record: AgentRecord) => void;
17
18
  export type OnAgentStart = (record: AgentRecord) => void;
19
+ export type OnAgentCompact = (record: AgentRecord, info: CompactionInfo) => void;
20
+ export type CompactionInfo = { reason: "manual" | "threshold" | "overflow"; tokensBefore: number };
18
21
 
19
22
  /** Default max concurrent background agents. */
20
23
  const DEFAULT_MAX_CONCURRENT = 4;
@@ -35,8 +38,16 @@ interface SpawnOptions {
35
38
  inheritContext?: boolean;
36
39
  thinkingLevel?: ThinkingLevel;
37
40
  isBackground?: boolean;
41
+ /**
42
+ * Skip the maxConcurrent queue check for this spawn — start immediately even
43
+ * if the configured concurrency limit would otherwise queue it. Used by the
44
+ * scheduler so a fired job can't be deferred past its trigger window.
45
+ */
46
+ bypassQueue?: boolean;
38
47
  /** Isolation mode — "worktree" creates a temp git worktree for the agent. */
39
48
  isolation?: IsolationMode;
49
+ /** Parent abort signal — when aborted, the subagent is also stopped. */
50
+ signal?: AbortSignal;
40
51
  /** Called on tool start/end with activity info (for streaming progress to UI). */
41
52
  onToolActivity?: (activity: ToolActivity) => void;
42
53
  /** Called on streaming text deltas from the assistant response. */
@@ -45,6 +56,10 @@ interface SpawnOptions {
45
56
  onSessionCreated?: (session: AgentSession) => void;
46
57
  /** Called at the end of each agentic turn with the cumulative count. */
47
58
  onTurnEnd?: (turnCount: number) => void;
59
+ /** Called once per assistant message_end with that message's usage delta. */
60
+ onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
61
+ /** Called when the session successfully compacts. */
62
+ onCompaction?: (info: CompactionInfo) => void;
48
63
  }
49
64
 
50
65
  export class AgentManager {
@@ -52,6 +67,7 @@ export class AgentManager {
52
67
  private cleanupInterval: ReturnType<typeof setInterval>;
53
68
  private onComplete?: OnAgentComplete;
54
69
  private onStart?: OnAgentStart;
70
+ private onCompact?: OnAgentCompact;
55
71
  private maxConcurrent: number;
56
72
 
57
73
  /** Queue of background agents waiting to start. */
@@ -59,9 +75,15 @@ export class AgentManager {
59
75
  /** Number of currently running background agents. */
60
76
  private runningBackground = 0;
61
77
 
62
- constructor(onComplete?: OnAgentComplete, maxConcurrent = DEFAULT_MAX_CONCURRENT, onStart?: OnAgentStart) {
78
+ constructor(
79
+ onComplete?: OnAgentComplete,
80
+ maxConcurrent = DEFAULT_MAX_CONCURRENT,
81
+ onStart?: OnAgentStart,
82
+ onCompact?: OnAgentCompact,
83
+ ) {
63
84
  this.onComplete = onComplete;
64
85
  this.onStart = onStart;
86
+ this.onCompact = onCompact;
65
87
  this.maxConcurrent = maxConcurrent;
66
88
  // Cleanup completed agents after 10 minutes (but keep sessions for resume)
67
89
  this.cleanupInterval = setInterval(() => this.cleanup(), 60_000);
@@ -99,12 +121,14 @@ export class AgentManager {
99
121
  toolUses: 0,
100
122
  startedAt: Date.now(),
101
123
  abortController,
124
+ lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
125
+ compactionCount: 0,
102
126
  };
103
127
  this.agents.set(id, record);
104
128
 
105
129
  const args: SpawnArgs = { pi, ctx, type, prompt, options };
106
130
 
107
- if (options.isBackground && this.runningBackground >= this.maxConcurrent) {
131
+ if (options.isBackground && !options.bypassQueue && this.runningBackground >= this.maxConcurrent) {
108
132
  // Queue it — will be started when a running agent completes
109
133
  this.queue.push({ id, args });
110
134
  return id;
@@ -121,6 +145,15 @@ export class AgentManager {
121
145
  if (options.isBackground) this.runningBackground++;
122
146
  this.onStart?.(record);
123
147
 
148
+ // Wire parent abort signal to stop the subagent when the parent is interrupted
149
+ let detachParentSignal: (() => void) | undefined;
150
+ if (options.signal) {
151
+ const onParentAbort = () => this.abort(id);
152
+ options.signal.addEventListener("abort", onParentAbort, { once: true });
153
+ detachParentSignal = () => options.signal!.removeEventListener("abort", onParentAbort);
154
+ }
155
+ const detach = () => { detachParentSignal?.(); detachParentSignal = undefined; };
156
+
124
157
  // Worktree isolation: create a temporary git worktree if requested
125
158
  let worktreeCwd: string | undefined;
126
159
  let worktreeWarning = "";
@@ -152,6 +185,15 @@ export class AgentManager {
152
185
  },
153
186
  onTurnEnd: options.onTurnEnd,
154
187
  onTextDelta: options.onTextDelta,
188
+ onAssistantUsage: (usage) => {
189
+ addUsage(record.lifetimeUsage, usage);
190
+ options.onAssistantUsage?.(usage);
191
+ },
192
+ onCompaction: (info) => {
193
+ record.compactionCount++;
194
+ this.onCompact?.(record, info);
195
+ options.onCompaction?.(info);
196
+ },
155
197
  onSessionCreated: (session) => {
156
198
  record.session = session;
157
199
  // Flush any steers that arrived before the session was ready
@@ -173,6 +215,8 @@ export class AgentManager {
173
215
  record.session = session;
174
216
  record.completedAt ??= Date.now();
175
217
 
218
+ detach();
219
+
176
220
  // Final flush of streaming output file
177
221
  if (record.outputCleanup) {
178
222
  try { record.outputCleanup(); } catch { /* ignore */ }
@@ -204,6 +248,8 @@ export class AgentManager {
204
248
  record.error = err instanceof Error ? err.message : String(err);
205
249
  record.completedAt ??= Date.now();
206
250
 
251
+ detach();
252
+
207
253
  // Final flush of streaming output file on error
208
254
  if (record.outputCleanup) {
209
255
  try { record.outputCleanup(); } catch { /* ignore */ }
@@ -278,6 +324,13 @@ export class AgentManager {
278
324
  onToolActivity: (activity) => {
279
325
  if (activity.type === "end") record.toolUses++;
280
326
  },
327
+ onAssistantUsage: (usage) => {
328
+ addUsage(record.lifetimeUsage, usage);
329
+ },
330
+ onCompaction: (info) => {
331
+ record.compactionCount++;
332
+ this.onCompact?.(record, info);
333
+ },
281
334
  signal,
282
335
  });
283
336
  record.status = "completed";
@@ -103,6 +103,17 @@ export interface RunOptions {
103
103
  onSessionCreated?: (session: AgentSession) => void;
104
104
  /** Called at the end of each agentic turn with the cumulative count. */
105
105
  onTurnEnd?: (turnCount: number) => void;
106
+ /**
107
+ * Called once per assistant message_end with that message's usage delta.
108
+ * Lets callers maintain a lifetime accumulator that survives compaction
109
+ * (which replaces session.state.messages and resets stats-derived sums).
110
+ */
111
+ onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
112
+ /**
113
+ * Called when the session successfully compacts. `tokensBefore` is upstream's
114
+ * pre-compaction context size estimate. Aborted compactions don't fire.
115
+ */
116
+ onCompaction?: (info: { reason: "manual" | "threshold" | "overflow"; tokensBefore: number }) => void;
106
117
  }
107
118
 
108
119
  export interface RunResult {
@@ -343,6 +354,17 @@ export async function runAgent(
343
354
  if (event.type === "tool_execution_end") {
344
355
  options.onToolActivity?.({ type: "end", toolName: event.toolName });
345
356
  }
357
+ if (event.type === "message_end" && event.message.role === "assistant") {
358
+ const u = (event.message as any).usage;
359
+ if (u) options.onAssistantUsage?.({
360
+ input: u.input ?? 0,
361
+ output: u.output ?? 0,
362
+ cacheWrite: u.cacheWrite ?? 0,
363
+ });
364
+ }
365
+ if (event.type === "compaction_end" && !event.aborted && event.result) {
366
+ options.onCompaction?.({ reason: event.reason, tokensBefore: event.result.tokensBefore });
367
+ }
346
368
  });
347
369
 
348
370
  const collector = collectResponseText(session);
@@ -375,15 +397,31 @@ export async function runAgent(
375
397
  export async function resumeAgent(
376
398
  session: AgentSession,
377
399
  prompt: string,
378
- options: { onToolActivity?: (activity: ToolActivity) => void; signal?: AbortSignal } = {},
400
+ options: {
401
+ onToolActivity?: (activity: ToolActivity) => void;
402
+ onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
403
+ onCompaction?: (info: { reason: "manual" | "threshold" | "overflow"; tokensBefore: number }) => void;
404
+ signal?: AbortSignal;
405
+ } = {},
379
406
  ): Promise<string> {
380
407
  const collector = collectResponseText(session);
381
408
  const cleanupAbort = forwardAbortSignal(session, options.signal);
382
409
 
383
- const unsubToolUse = options.onToolActivity
410
+ const unsubEvents = (options.onToolActivity || options.onAssistantUsage || options.onCompaction)
384
411
  ? session.subscribe((event: AgentSessionEvent) => {
385
- if (event.type === "tool_execution_start") options.onToolActivity!({ type: "start", toolName: event.toolName });
386
- if (event.type === "tool_execution_end") options.onToolActivity!({ type: "end", toolName: event.toolName });
412
+ if (event.type === "tool_execution_start") options.onToolActivity?.({ type: "start", toolName: event.toolName });
413
+ if (event.type === "tool_execution_end") options.onToolActivity?.({ type: "end", toolName: event.toolName });
414
+ if (event.type === "message_end" && event.message.role === "assistant") {
415
+ const u = (event.message as any).usage;
416
+ if (u) options.onAssistantUsage?.({
417
+ input: u.input ?? 0,
418
+ output: u.output ?? 0,
419
+ cacheWrite: u.cacheWrite ?? 0,
420
+ });
421
+ }
422
+ if (event.type === "compaction_end" && !event.aborted && event.result) {
423
+ options.onCompaction?.({ reason: event.reason, tokensBefore: event.result.tokensBefore });
424
+ }
387
425
  })
388
426
  : () => {};
389
427
 
@@ -391,7 +429,7 @@ export async function resumeAgent(
391
429
  await session.prompt(prompt);
392
430
  } finally {
393
431
  collector.unsubscribe();
394
- unsubToolUse();
432
+ unsubEvents();
395
433
  cleanupAbort();
396
434
  }
397
435