@aliou/pi-processes 0.10.2 → 0.10.4
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/extensions/processes/handlers/notifications.ts +5 -4
- package/extensions/processes/notifications/log-matchers.ts +7 -2
- package/extensions/processes/notifications/service.ts +4 -1
- package/extensions/processes/tools/index.ts +2 -1
- package/extensions/processes/tools/notify.ts +13 -4
- package/extensions/processes/tools/schema.ts +10 -8
- package/extensions/processes-logs/components/log-file-viewer.ts +14 -5
- package/extensions/shared/display-text.ts +83 -18
- package/extensions/shared/log-line.ts +7 -9
- package/package.json +1 -1
- package/skills/pi-processes/SKILL.md +16 -6
|
@@ -60,10 +60,11 @@ export function registerNotificationDelivery(
|
|
|
60
60
|
summary: `Suppressed ${count} log-match notifications because output was too fast.`,
|
|
61
61
|
attention: "context",
|
|
62
62
|
};
|
|
63
|
-
sendProcessNotificationMessage(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
sendProcessNotificationMessage(
|
|
64
|
+
pi,
|
|
65
|
+
details,
|
|
66
|
+
attentionToSendOptions(details.attention),
|
|
67
|
+
);
|
|
67
68
|
};
|
|
68
69
|
|
|
69
70
|
const flushSuppressedSummary = () => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { compileLineMatcher } from "../../../src/utils/match-line";
|
|
2
|
+
import { plainTextForDisplay } from "../../shared/display-text";
|
|
2
3
|
import type { NotifyConfig } from "./registry";
|
|
3
4
|
import type { Attention } from "./types";
|
|
4
5
|
|
|
@@ -130,8 +131,12 @@ function splitAppendedIntoLines(
|
|
|
130
131
|
for (const entry of appended) {
|
|
131
132
|
const split = entry.text.split("\n");
|
|
132
133
|
for (const line of split) {
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
// Match the same per-line visible text the log views render. This keeps
|
|
135
|
+
// CR-overwritten progress and invisible escape bytes from firing
|
|
136
|
+
// watches, without making watches depend on viewport width.
|
|
137
|
+
const displayLine = plainTextForDisplay(line);
|
|
138
|
+
if (displayLine.length === 0) continue;
|
|
139
|
+
lines.push({ type: entry.type, text: displayLine });
|
|
135
140
|
}
|
|
136
141
|
}
|
|
137
142
|
|
|
@@ -25,7 +25,10 @@ const DEFAULT_ATTENTION: Record<
|
|
|
25
25
|
"onSuccess" | "onFailure" | "onKilled",
|
|
26
26
|
Attention
|
|
27
27
|
> = {
|
|
28
|
-
|
|
28
|
+
// Keep in sync with DEFAULT_NOTIFY_CONFIG in tools/notify.ts. Success
|
|
29
|
+
// defaults to a turn because "context" is only seen by an agent that is
|
|
30
|
+
// still streaming when the process ends.
|
|
31
|
+
onSuccess: "turn",
|
|
29
32
|
onFailure: "turn",
|
|
30
33
|
// External kills surface as context by default (see DEFAULT_NOTIFY_CONFIG
|
|
31
34
|
// in tools/notify.ts for rationale). Intentional stops are classified
|
|
@@ -54,7 +54,8 @@ export function registerProcessTool(
|
|
|
54
54
|
"Manage long-running background processes: start, list, stop, write to stdin, update watches, clear finished entries, and inspect recent output. After starting a process, do not wait - notifications bring you back on exit and on log matches.",
|
|
55
55
|
promptGuidelines: [
|
|
56
56
|
"process tool: use process start for long-running commands (dev servers, watchers, builds) instead of shell background patterns like &, nohup, or setsid; give each process a specific name and check process list first when a duplicate would be noisy.",
|
|
57
|
-
"process tool: after process start, do not sleep, poll, or hold your turn. End your turn or move on.
|
|
57
|
+
"process tool: after process start, do not sleep, poll, or hold your turn. End your turn or move on. Exits and notify.logMatches matches bring you back.",
|
|
58
|
+
"process tool: attention turn wakes you when idle; context only reaches you if you are still working; ignore never notifies. Keep turn for anything whose result you need.",
|
|
58
59
|
"process tool: use notify.logMatches to get brought back on readiness or error signals instead of polling process output. If a watch is too noisy, use process update (watches.mode append/replace/remove/clear) to fix it without restarting.",
|
|
59
60
|
"process tool: for the full lifecycle (start, list, output, update, write, stop, clear), notify options, use cases, and noisy-watch handling, read the pi-processes skill.",
|
|
60
61
|
],
|
|
@@ -14,7 +14,13 @@ export const MAX_NOTIFY_LOG_MATCHERS = MAX_LOG_MATCHERS_PER_PROCESS;
|
|
|
14
14
|
export const MAX_NOTIFY_PATTERN_LENGTH = MAX_LOG_MATCH_PATTERN_LENGTH;
|
|
15
15
|
|
|
16
16
|
const DEFAULT_NOTIFY_CONFIG = {
|
|
17
|
-
|
|
17
|
+
// A backgrounded process usually outlives the turn that started it, and
|
|
18
|
+
// "context" only reaches the agent if it happens to still be streaming when
|
|
19
|
+
// the process ends. Builds, tests, and other one-shot commands are started
|
|
20
|
+
// precisely because the agent needs the result, so success defaults to a
|
|
21
|
+
// turn. Long-running servers rarely exit 0, and callers that do not want the
|
|
22
|
+
// interruption can pass onSuccess: "context".
|
|
23
|
+
onSuccess: "turn",
|
|
18
24
|
onFailure: "turn",
|
|
19
25
|
// External kills (outside this manager) surface as context by default so
|
|
20
26
|
// the agent and user learn that a managed process disappeared. Intentional
|
|
@@ -36,11 +42,14 @@ export function normalizeNotifyConfig(input: unknown): NotifyConfig {
|
|
|
36
42
|
|
|
37
43
|
return {
|
|
38
44
|
onSuccess:
|
|
39
|
-
normalizeAttention(input.onSuccess, "notify.onSuccess") ??
|
|
45
|
+
normalizeAttention(input.onSuccess, "notify.onSuccess") ??
|
|
46
|
+
DEFAULT_NOTIFY_CONFIG.onSuccess,
|
|
40
47
|
onFailure:
|
|
41
|
-
normalizeAttention(input.onFailure, "notify.onFailure") ??
|
|
48
|
+
normalizeAttention(input.onFailure, "notify.onFailure") ??
|
|
49
|
+
DEFAULT_NOTIFY_CONFIG.onFailure,
|
|
42
50
|
onKilled:
|
|
43
|
-
normalizeAttention(input.onKilled, "notify.onKilled") ??
|
|
51
|
+
normalizeAttention(input.onKilled, "notify.onKilled") ??
|
|
52
|
+
DEFAULT_NOTIFY_CONFIG.onKilled,
|
|
44
53
|
logMatches: normalizeLogMatches(logMatches),
|
|
45
54
|
};
|
|
46
55
|
}
|
|
@@ -110,28 +110,25 @@ const WatchUpdateItemParams = Type.Object({
|
|
|
110
110
|
),
|
|
111
111
|
on: Type.Optional(
|
|
112
112
|
StringEnum(PROCESS_NOTIFY_ATTENTIONS, {
|
|
113
|
-
description: "
|
|
113
|
+
description: "Attention for this match. Defaults to turn.",
|
|
114
114
|
}),
|
|
115
115
|
),
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
-
const
|
|
118
|
+
const NotifyProperties = {
|
|
119
119
|
onSuccess: Type.Optional(
|
|
120
120
|
StringEnum(PROCESS_NOTIFY_ATTENTIONS, {
|
|
121
|
-
description:
|
|
122
|
-
"Agent attention when the process exits successfully. Defaults to context.",
|
|
121
|
+
description: "Attention on clean exit. Defaults to turn.",
|
|
123
122
|
}),
|
|
124
123
|
),
|
|
125
124
|
onFailure: Type.Optional(
|
|
126
125
|
StringEnum(PROCESS_NOTIFY_ATTENTIONS, {
|
|
127
|
-
description:
|
|
128
|
-
"Agent attention when the process fails or crashes. Defaults to turn.",
|
|
126
|
+
description: "Attention on failure or crash. Defaults to turn.",
|
|
129
127
|
}),
|
|
130
128
|
),
|
|
131
129
|
onKilled: Type.Optional(
|
|
132
130
|
StringEnum(PROCESS_NOTIFY_ATTENTIONS, {
|
|
133
|
-
description:
|
|
134
|
-
"Agent attention when the process is killed. Defaults to context.",
|
|
131
|
+
description: "Attention on external kill. Defaults to context.",
|
|
135
132
|
}),
|
|
136
133
|
),
|
|
137
134
|
logMatches: Type.Optional(
|
|
@@ -141,6 +138,11 @@ const NotifyParams = Type.Object({
|
|
|
141
138
|
"Log match notifications. Supports at most 20 matchers, with each pattern limited to 500 characters.",
|
|
142
139
|
}),
|
|
143
140
|
),
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const NotifyParams = Type.Object(NotifyProperties, {
|
|
144
|
+
description:
|
|
145
|
+
"Notify settings. Attention: turn wakes an idle agent, context only reaches an agent still working, ignore never notifies.",
|
|
144
146
|
});
|
|
145
147
|
|
|
146
148
|
export const ProcessesParams = Type.Object({
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
plainTextForDisplay,
|
|
5
|
+
sanitizeForDisplay,
|
|
6
|
+
} from "../../shared/display-text";
|
|
4
7
|
import { trimToBudget } from "../../shared/line-buffer";
|
|
5
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
displayTextOf,
|
|
10
|
+
type LogLineEmphasis,
|
|
11
|
+
renderLogLine,
|
|
12
|
+
} from "../../shared/log-line";
|
|
6
13
|
import { truncateToWidth } from "../../shared/truncate";
|
|
7
14
|
import type { ProcessLogLine } from "../logs-client";
|
|
8
15
|
|
|
@@ -122,8 +129,9 @@ export class LogFileViewer {
|
|
|
122
129
|
* priority (search current match > search match > notify match > stream).
|
|
123
130
|
*/
|
|
124
131
|
addNotifyMatch(match: { line: string }): void {
|
|
125
|
-
// Stored
|
|
126
|
-
|
|
132
|
+
// Stored as plain visible text so invisible escape bytes cannot be what
|
|
133
|
+
// makes a notification marker match.
|
|
134
|
+
const line = plainTextForDisplay(match.line);
|
|
127
135
|
if (line) this.notifyLines.add(line);
|
|
128
136
|
}
|
|
129
137
|
|
|
@@ -192,7 +200,8 @@ export class LogFileViewer {
|
|
|
192
200
|
? "search-current"
|
|
193
201
|
: matchSet.has(visibleIndex)
|
|
194
202
|
? "search"
|
|
195
|
-
: this.notifyLines.has(line.text)
|
|
203
|
+
: this.notifyLines.has(line.text) ||
|
|
204
|
+
this.notifyLines.has(displayTextOf(line))
|
|
196
205
|
? "notify"
|
|
197
206
|
: "none";
|
|
198
207
|
return renderLogLine(line, { theme: this.theme, width, emphasis });
|
|
@@ -13,10 +13,17 @@ const ESC = String.fromCodePoint(0x001b);
|
|
|
13
13
|
const BEL = String.fromCodePoint(0x0007);
|
|
14
14
|
const ST = String.fromCodePoint(0x009c);
|
|
15
15
|
const RESET = `${ESC}[0m`;
|
|
16
|
+
const C1_DCS = String.fromCodePoint(0x0090);
|
|
17
|
+
const C1_SOS = String.fromCodePoint(0x0098);
|
|
18
|
+
const C1_OSC = String.fromCodePoint(0x009d);
|
|
19
|
+
const C1_PM = String.fromCodePoint(0x009e);
|
|
20
|
+
const C1_APC = String.fromCodePoint(0x009f);
|
|
21
|
+
const C1_STRING_INTRODUCERS = [C1_DCS, C1_SOS, C1_OSC, C1_PM, C1_APC];
|
|
16
22
|
|
|
17
|
-
// Control characters a single display row must never contain. Tabs
|
|
18
|
-
// separately; newlines are dropped because they
|
|
19
|
-
// C1 controls are included: a raw \u009b is an
|
|
23
|
+
// Control characters a single display row must never contain. Tabs and
|
|
24
|
+
// carriage returns are handled separately; newlines are dropped because they
|
|
25
|
+
// would shift the whole frame. C1 controls are included: a raw \u009b is an
|
|
26
|
+
// alias for CSI on some terminals.
|
|
20
27
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: this regex intentionally targets terminal control characters.
|
|
21
28
|
const DISPLAY_CONTROL_CHARS = /[\u0000-\u0008\u000a-\u001f\u007f-\u009f]/gu;
|
|
22
29
|
|
|
@@ -41,7 +48,9 @@ const TAB_WIDTH = 8;
|
|
|
41
48
|
* into the rest of the frame.
|
|
42
49
|
*/
|
|
43
50
|
export function sanitizeForDisplay(text: string): string {
|
|
44
|
-
if (!text.includes(ESC)
|
|
51
|
+
if (!text.includes(ESC) && !hasC1StringIntroducer(text)) {
|
|
52
|
+
return cleanPlainText(text, 0).text;
|
|
53
|
+
}
|
|
45
54
|
|
|
46
55
|
let out = "";
|
|
47
56
|
let cursor = 0;
|
|
@@ -49,21 +58,34 @@ export function sanitizeForDisplay(text: string): string {
|
|
|
49
58
|
let keptSgr = false;
|
|
50
59
|
|
|
51
60
|
while (cursor < text.length) {
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
54
|
-
|
|
61
|
+
const sequenceAt = findNextSequenceStart(text, cursor);
|
|
62
|
+
if (sequenceAt === -1) {
|
|
63
|
+
const chunk = cleanPlainText(text.slice(cursor), column);
|
|
64
|
+
if (chunk.resetsRow) {
|
|
65
|
+
out = "";
|
|
66
|
+
keptSgr = false;
|
|
67
|
+
}
|
|
68
|
+
out += chunk.text;
|
|
55
69
|
break;
|
|
56
70
|
}
|
|
57
|
-
const chunk = cleanPlainText(text.slice(cursor,
|
|
71
|
+
const chunk = cleanPlainText(text.slice(cursor, sequenceAt), column);
|
|
72
|
+
if (chunk.resetsRow) {
|
|
73
|
+
out = "";
|
|
74
|
+
keptSgr = false;
|
|
75
|
+
}
|
|
58
76
|
out += chunk.text;
|
|
59
77
|
column = chunk.column;
|
|
60
78
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
79
|
+
if (text[sequenceAt] === ESC) {
|
|
80
|
+
const sequence = readEscapeSequence(text, sequenceAt);
|
|
81
|
+
if (sequence.isSgr) {
|
|
82
|
+
out += text.slice(sequenceAt, sequence.end);
|
|
83
|
+
keptSgr = true;
|
|
84
|
+
}
|
|
85
|
+
cursor = sequence.end;
|
|
86
|
+
} else {
|
|
87
|
+
cursor = readC1StringSequence(text, sequenceAt).end;
|
|
65
88
|
}
|
|
66
|
-
cursor = sequence.end;
|
|
67
89
|
}
|
|
68
90
|
|
|
69
91
|
if (!keptSgr || out.endsWith(RESET)) return out;
|
|
@@ -79,6 +101,22 @@ export function truncateForDisplay(text: string, width: number): string {
|
|
|
79
101
|
return closeSgr(truncateToWidth(sanitizeForDisplay(text), width, "…"));
|
|
80
102
|
}
|
|
81
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Return the unstyled text a user can actually read after terminal controls
|
|
106
|
+
* have been interpreted/dropped. Use this for comparisons such as search,
|
|
107
|
+
* notify markers, and log watches so invisible escape bytes cannot match.
|
|
108
|
+
*/
|
|
109
|
+
export function plainTextForDisplay(text: string): string {
|
|
110
|
+
return stripSgr(sanitizeForDisplay(text));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const SGR = new RegExp(`${ESC}\\[[0-9;:]*m`, "gu");
|
|
114
|
+
|
|
115
|
+
/** Drop the SGR sequences `sanitizeForDisplay` kept for rendering colors. */
|
|
116
|
+
export function stripSgr(text: string): string {
|
|
117
|
+
return text.replace(SGR, "");
|
|
118
|
+
}
|
|
119
|
+
|
|
82
120
|
/**
|
|
83
121
|
* Re-close colors after truncation. `sanitizeForDisplay` ends a colored string
|
|
84
122
|
* with a reset, but truncating can cut that reset off and let the color bleed
|
|
@@ -97,21 +135,42 @@ export function closeSgr(text: string): string {
|
|
|
97
135
|
function cleanPlainText(
|
|
98
136
|
text: string,
|
|
99
137
|
column: number,
|
|
100
|
-
): { text: string; column: number } {
|
|
101
|
-
const
|
|
138
|
+
): { text: string; column: number; resetsRow: boolean } {
|
|
139
|
+
const carriageReturn = text.lastIndexOf("\r");
|
|
140
|
+
const resetsRow = carriageReturn !== -1;
|
|
141
|
+
const visibleText = resetsRow ? text.slice(carriageReturn + 1) : text;
|
|
142
|
+
const startColumn = resetsRow ? 0 : column;
|
|
143
|
+
const clean = visibleText.replace(DISPLAY_CONTROL_CHARS, "");
|
|
102
144
|
if (!clean.includes("\t")) {
|
|
103
|
-
return {
|
|
145
|
+
return {
|
|
146
|
+
text: clean,
|
|
147
|
+
column: startColumn + visibleWidth(clean),
|
|
148
|
+
resetsRow,
|
|
149
|
+
};
|
|
104
150
|
}
|
|
105
151
|
|
|
106
152
|
const parts = clean.split("\t");
|
|
107
153
|
let out = parts[0] ?? "";
|
|
108
|
-
let col =
|
|
154
|
+
let col = startColumn + visibleWidth(out);
|
|
109
155
|
for (const part of parts.slice(1)) {
|
|
110
156
|
const spaces = TAB_WIDTH - (col % TAB_WIDTH);
|
|
111
157
|
out += " ".repeat(spaces) + part;
|
|
112
158
|
col += spaces + visibleWidth(part);
|
|
113
159
|
}
|
|
114
|
-
return { text: out, column: col };
|
|
160
|
+
return { text: out, column: col, resetsRow };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function findNextSequenceStart(text: string, cursor: number): number {
|
|
164
|
+
let next = text.indexOf(ESC, cursor);
|
|
165
|
+
for (const introducer of C1_STRING_INTRODUCERS) {
|
|
166
|
+
const index = text.indexOf(introducer, cursor);
|
|
167
|
+
if (index !== -1 && (next === -1 || index < next)) next = index;
|
|
168
|
+
}
|
|
169
|
+
return next;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function hasC1StringIntroducer(text: string): boolean {
|
|
173
|
+
return C1_STRING_INTRODUCERS.some((introducer) => text.includes(introducer));
|
|
115
174
|
}
|
|
116
175
|
|
|
117
176
|
/**
|
|
@@ -151,6 +210,12 @@ function readEscapeSequence(
|
|
|
151
210
|
return { end: start + 2, isSgr: false };
|
|
152
211
|
}
|
|
153
212
|
|
|
213
|
+
function readC1StringSequence(text: string, start: number): { end: number } {
|
|
214
|
+
const introducer = text[start];
|
|
215
|
+
const allowBel = introducer === C1_OSC || introducer === C1_APC;
|
|
216
|
+
return { end: findStringTerminator(text, start + 1, allowBel) };
|
|
217
|
+
}
|
|
218
|
+
|
|
154
219
|
/** Index just past the terminator of a string sequence, else end of input. */
|
|
155
220
|
function findStringTerminator(
|
|
156
221
|
text: string,
|
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
closeSgr,
|
|
15
|
+
plainTextForDisplay,
|
|
16
|
+
sanitizeForDisplay,
|
|
17
|
+
stripSgr,
|
|
18
|
+
} from "./display-text";
|
|
14
19
|
import { truncateToWidth } from "./truncate";
|
|
15
20
|
|
|
16
21
|
export interface DisplayLogLine {
|
|
@@ -60,7 +65,7 @@ export function renderLogLine(
|
|
|
60
65
|
|
|
61
66
|
/** Text of a log line as the views display it, for match comparisons. */
|
|
62
67
|
export function displayTextOf(line: DisplayLogLine): string {
|
|
63
|
-
return
|
|
68
|
+
return plainTextForDisplay(line.text);
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
function toneLogText(
|
|
@@ -75,10 +80,3 @@ function toneLogText(
|
|
|
75
80
|
if (type === "stderr") return theme.fg("warning", text);
|
|
76
81
|
return text;
|
|
77
82
|
}
|
|
78
|
-
|
|
79
|
-
const SGR = new RegExp(`${String.fromCodePoint(0x001b)}\\[[0-9;:]*m`, "gu");
|
|
80
|
-
|
|
81
|
-
/** Drop the SGR sequences `sanitizeForDisplay` kept. */
|
|
82
|
-
function stripSgr(text: string): string {
|
|
83
|
-
return text.replace(SGR, "");
|
|
84
|
-
}
|
package/package.json
CHANGED
|
@@ -15,9 +15,11 @@ A started process runs in the background and the manager brings you back when so
|
|
|
15
15
|
2. End your turn or move on to other work. Do not call `process output` in a loop waiting for "ready".
|
|
16
16
|
3. The process notifies you when:
|
|
17
17
|
- a `logMatches` pattern hits (readiness, error, progress),
|
|
18
|
-
- the process exits successfully (`onSuccess`, default `
|
|
18
|
+
- the process exits successfully (`onSuccess`, default `turn`),
|
|
19
19
|
- the process fails or crashes (`onFailure`, default `turn`),
|
|
20
|
-
- the process is killed
|
|
20
|
+
- the process is killed externally (`onKilled`, default `context`, which does not wake an idle agent).
|
|
21
|
+
|
|
22
|
+
Stopping a process yourself never notifies.
|
|
21
23
|
4. When a watch is too noisy or wrong, fix it with `process update` — do not restart the process just to change watches.
|
|
22
24
|
5. `process stop` obsolete live processes and `process clear` finished entries when they are no longer useful.
|
|
23
25
|
|
|
@@ -50,6 +52,8 @@ Good:
|
|
|
50
52
|
}
|
|
51
53
|
```
|
|
52
54
|
|
|
55
|
+
`onSuccess: "context"` here because a dev server exiting cleanly needs no reaction. Keep the default `turn` for builds, tests, and other one-shot commands whose result you need.
|
|
56
|
+
|
|
53
57
|
Optional `cwd` sets the working directory for the spawned command. Omit it to inherit the agent's current working directory.
|
|
54
58
|
|
|
55
59
|
Empty `logMatches` patterns (literal or regex) are rejected at start and update time. Use `mode: "regex"` only when literal matching is not enough, scope by `stream` to cut noise, and use `repeat: true` when a matcher should fire more than once.
|
|
@@ -214,9 +218,9 @@ Good:
|
|
|
214
218
|
|
|
215
219
|
Exit attention:
|
|
216
220
|
|
|
217
|
-
- `notify.onSuccess` —
|
|
218
|
-
- `notify.onFailure` —
|
|
219
|
-
- `notify.onKilled` —
|
|
221
|
+
- `notify.onSuccess` — clean exit. Defaults to `turn`.
|
|
222
|
+
- `notify.onFailure` — failure or crash. Defaults to `turn`.
|
|
223
|
+
- `notify.onKilled` — killed from outside the tool. Defaults to `context`. Stopping a process yourself never notifies.
|
|
220
224
|
|
|
221
225
|
Log match watches (`notify.logMatches`, up to 20, each pattern up to 500 chars):
|
|
222
226
|
|
|
@@ -226,7 +230,13 @@ Log match watches (`notify.logMatches`, up to 20, each pattern up to 500 chars):
|
|
|
226
230
|
- `repeat` — `false` (default) fires once; `true` fires on every match.
|
|
227
231
|
- `on` — `turn`, `context`, or `ignore`. Overrides the default attention for that watch. Defaults to `turn`.
|
|
228
232
|
|
|
229
|
-
|
|
233
|
+
Attention levels:
|
|
234
|
+
|
|
235
|
+
- `turn` — starts an agent turn. Reaches you even when you are idle.
|
|
236
|
+
- `context` — recorded in the transcript, no turn. It reaches you only if you are still working when the event fires; an idle agent is not woken and sees it on the next user message.
|
|
237
|
+
- `ignore` — recorded, never notifies.
|
|
238
|
+
|
|
239
|
+
Use `context` only when nothing needs to happen in response.
|
|
230
240
|
|
|
231
241
|
## Use cases
|
|
232
242
|
|