macroclaw 0.26.0 → 0.28.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.
@@ -1,16 +1,16 @@
1
1
  import { describe, expect, it } from "bun:test";
2
- import { SYSTEM_PROMPT } from "./prompts";
2
+ import { buildEvent, escapeXml, SYSTEM_PROMPT } from "./prompts";
3
3
 
4
4
  describe("SYSTEM_PROMPT", () => {
5
5
  it("contains key sections", () => {
6
6
  expect(SYSTEM_PROMPT).toContain("macroclaw");
7
7
  expect(SYSTEM_PROMPT).toContain("Structured output");
8
- expect(SYSTEM_PROMPT).toContain("Context tags");
8
+ expect(SYSTEM_PROMPT).toContain("Event format");
9
9
  expect(SYSTEM_PROMPT).toContain("Background agents");
10
10
  expect(SYSTEM_PROMPT).toContain("Cron");
11
11
  expect(SYSTEM_PROMPT).toContain("Buttons");
12
12
  expect(SYSTEM_PROMPT).toContain("Files");
13
- expect(SYSTEM_PROMPT).toContain("Timeouts");
13
+ expect(SYSTEM_PROMPT).toContain("Session routing");
14
14
  });
15
15
 
16
16
  it("contains HTML formatting instructions", () => {
@@ -18,11 +18,19 @@ describe("SYSTEM_PROMPT", () => {
18
18
  expect(SYSTEM_PROMPT).toContain("<b>");
19
19
  });
20
20
 
21
- it("documents all context tag types", () => {
22
- expect(SYSTEM_PROMPT).toContain("cron/<name>");
21
+ it("documents all event types", () => {
22
+ expect(SYSTEM_PROMPT).toContain("user-message");
23
23
  expect(SYSTEM_PROMPT).toContain("button-click");
24
- expect(SYSTEM_PROMPT).toContain("background-result/<name>");
25
- expect(SYSTEM_PROMPT).toContain("background-agent/<name>");
24
+ expect(SYSTEM_PROMPT).toContain("schedule-trigger");
25
+ expect(SYSTEM_PROMPT).toContain("background-agent-start");
26
+ expect(SYSTEM_PROMPT).toContain("background-agent-result");
27
+ expect(SYSTEM_PROMPT).toContain("peek");
28
+ });
29
+
30
+ it("documents backgrounded events", () => {
31
+ expect(SYSTEM_PROMPT).toContain("backgrounded-event");
32
+ expect(SYSTEM_PROMPT).toContain("moved to background");
33
+ expect(SYSTEM_PROMPT).toContain("Do not re-execute");
26
34
  });
27
35
 
28
36
  it("contains structured output reinforcement", () => {
@@ -41,3 +49,243 @@ describe("SYSTEM_PROMPT", () => {
41
49
  expect(SYSTEM_PROMPT).toContain("opus");
42
50
  });
43
51
  });
52
+
53
+ describe("escapeXml", () => {
54
+ it("escapes &, <, >, \"", () => {
55
+ expect(escapeXml('a & b < c > d "e"')).toBe("a &amp; b &lt; c &gt; d &quot;e&quot;");
56
+ });
57
+
58
+ it("returns plain text unchanged", () => {
59
+ expect(escapeXml("hello world")).toBe("hello world");
60
+ });
61
+ });
62
+
63
+ describe("buildEvent", () => {
64
+ it("builds user message event", () => {
65
+ const result = buildEvent({
66
+ name: "check-logs",
67
+ type: "user-message",
68
+ session: "main",
69
+ text: "hello",
70
+ });
71
+ expect(result).toStartWith('<event name="check-logs" type="user-message" session="main">');
72
+ expect(result).toContain("<text>hello</text>");
73
+ expect(result).toEndWith("</event>");
74
+ });
75
+
76
+ it("builds user message with files", () => {
77
+ const result = buildEvent({
78
+ name: "analyze-photo",
79
+ type: "user-message",
80
+ session: "main",
81
+ text: "what's in this image?",
82
+ files: ["/tmp/photo.jpg", "/tmp/doc.pdf"],
83
+ });
84
+ expect(result).toContain("<text>what's in this image?</text>");
85
+ expect(result).toContain("<files>");
86
+ expect(result).toContain('<file path="/tmp/photo.jpg" />');
87
+ expect(result).toContain('<file path="/tmp/doc.pdf" />');
88
+ expect(result).toContain("</files>");
89
+ });
90
+
91
+ it("builds user message with files only (no text)", () => {
92
+ const result = buildEvent({
93
+ name: "task",
94
+ type: "user-message",
95
+ session: "main",
96
+ files: ["/tmp/photo.jpg"],
97
+ });
98
+ expect(result).not.toContain("<text>");
99
+ expect(result).toContain('<file path="/tmp/photo.jpg" />');
100
+ });
101
+
102
+ it("builds user message with backgrounded event", () => {
103
+ const result = buildEvent({
104
+ name: "check-logs",
105
+ type: "user-message",
106
+ session: "main",
107
+ backgroundedEvent: "deploy-cluster",
108
+ text: "check the logs",
109
+ });
110
+ expect(result).toContain('<backgrounded-event name="deploy-cluster" />');
111
+ expect(result).toContain("<text>check the logs</text>");
112
+ });
113
+
114
+ it("places backgrounded-event before text", () => {
115
+ const result = buildEvent({
116
+ name: "check-logs",
117
+ type: "user-message",
118
+ session: "main",
119
+ backgroundedEvent: "deploy",
120
+ text: "hello",
121
+ });
122
+ const bgIdx = result.indexOf("backgrounded-event");
123
+ const textIdx = result.indexOf("<text>");
124
+ expect(bgIdx).toBeLessThan(textIdx);
125
+ });
126
+
127
+ it("builds button click event", () => {
128
+ const result = buildEvent({
129
+ name: "btn-yes",
130
+ type: "button-click",
131
+ session: "main",
132
+ button: "Yes",
133
+ });
134
+ expect(result).toContain('type="button-click"');
135
+ expect(result).toContain("<button>Yes</button>");
136
+ expect(result).not.toContain("<text>");
137
+ });
138
+
139
+ it("builds button click with backgrounded event", () => {
140
+ const result = buildEvent({
141
+ name: "btn-yes",
142
+ type: "button-click",
143
+ session: "main",
144
+ button: "Yes",
145
+ backgroundedEvent: "deploy-cluster",
146
+ });
147
+ expect(result).toContain('<backgrounded-event name="deploy-cluster" />');
148
+ expect(result).toContain("<button>Yes</button>");
149
+ });
150
+
151
+ it("builds schedule trigger event", () => {
152
+ const result = buildEvent({
153
+ name: "cron-daily",
154
+ type: "schedule-trigger",
155
+ session: "background",
156
+ schedule: { name: "daily" },
157
+ text: "check updates",
158
+ });
159
+ expect(result).toContain('type="schedule-trigger"');
160
+ expect(result).toContain('session="background"');
161
+ expect(result).toContain('<schedule name="daily" />');
162
+ expect(result).toContain("<text>check updates</text>");
163
+ });
164
+
165
+ it("builds missed schedule trigger with attributes", () => {
166
+ const result = buildEvent({
167
+ name: "cron-reminder",
168
+ type: "schedule-trigger",
169
+ session: "background",
170
+ schedule: { name: "reminder", missedBy: "15m", scheduledAt: "2026-03-20T06:00:00Z" },
171
+ text: "buy milk",
172
+ });
173
+ expect(result).toContain('missed-by="15m"');
174
+ expect(result).toContain('scheduled-at="2026-03-20T06:00:00Z"');
175
+ expect(result).toContain("<text>buy milk</text>");
176
+ });
177
+
178
+ it("builds background agent start event", () => {
179
+ const result = buildEvent({
180
+ name: "research",
181
+ type: "background-agent-start",
182
+ session: "background",
183
+ text: "find papers about transformers",
184
+ });
185
+ expect(result).toContain('type="background-agent-start"');
186
+ expect(result).toContain('session="background"');
187
+ expect(result).toContain("<text>find papers about transformers</text>");
188
+ });
189
+
190
+ it("builds background agent result (text only)", () => {
191
+ const result = buildEvent({
192
+ name: "bg-research",
193
+ type: "background-agent-result",
194
+ session: "main",
195
+ originalEvent: "research",
196
+ result: { text: "found 3 papers" },
197
+ });
198
+ expect(result).toContain('type="background-agent-result"');
199
+ expect(result).toContain('<original-event name="research" />');
200
+ expect(result).toContain("<result>");
201
+ expect(result).toContain("<text>found 3 papers</text>");
202
+ expect(result).toContain("</result>");
203
+ expect(result).not.toContain("<files>");
204
+ });
205
+
206
+ it("builds background agent result with files", () => {
207
+ const result = buildEvent({
208
+ name: "bg-research",
209
+ type: "background-agent-result",
210
+ session: "main",
211
+ originalEvent: "research",
212
+ result: { text: "here are the screenshots", files: ["/tmp/screenshot.png"] },
213
+ });
214
+ expect(result).toContain("<result>");
215
+ expect(result).toContain("<text>here are the screenshots</text>");
216
+ expect(result).toContain('<file path="/tmp/screenshot.png" />');
217
+ expect(result).toContain("</result>");
218
+ });
219
+
220
+ it("builds peek event with instructions", () => {
221
+ const result = buildEvent({
222
+ name: "peek-deploy",
223
+ type: "peek",
224
+ session: "background",
225
+ targetEvent: "deploy",
226
+ instructions: "Brief status update.",
227
+ });
228
+ expect(result).toContain('type="peek"');
229
+ expect(result).toContain('<target-event name="deploy" />');
230
+ expect(result).toContain("<instructions>Brief status update.</instructions>");
231
+ expect(result).not.toContain("<text>");
232
+ });
233
+
234
+ it("includes instructions in event", () => {
235
+ const result = buildEvent({
236
+ name: "bg-research",
237
+ type: "background-agent-result",
238
+ session: "main",
239
+ originalEvent: "research",
240
+ result: { text: "done" },
241
+ instructions: "Forward to user.",
242
+ });
243
+ expect(result).toContain("<instructions>Forward to user.</instructions>");
244
+ // instructions come last, before </event>
245
+ const instrIdx = result.indexOf("<instructions>");
246
+ const closeIdx = result.indexOf("</event>");
247
+ expect(instrIdx).toBeLessThan(closeIdx);
248
+ expect(instrIdx).toBeGreaterThan(result.indexOf("</result>"));
249
+ });
250
+
251
+ it("escapes XML in text content", () => {
252
+ const result = buildEvent({
253
+ name: "test",
254
+ type: "user-message",
255
+ session: "main",
256
+ text: "a < b & c > d",
257
+ });
258
+ expect(result).toContain("<text>a &lt; b &amp; c &gt; d</text>");
259
+ });
260
+
261
+ it("escapes XML in name attribute", () => {
262
+ const result = buildEvent({
263
+ name: 'a & "b"',
264
+ type: "user-message",
265
+ session: "main",
266
+ text: "test",
267
+ });
268
+ expect(result).toContain('name="a &amp; &quot;b&quot;"');
269
+ });
270
+
271
+ it("escapes XML in button label", () => {
272
+ const result = buildEvent({
273
+ name: "btn",
274
+ type: "button-click",
275
+ session: "main",
276
+ button: 'a & "b"',
277
+ });
278
+ expect(result).toContain("<button>a &amp; &quot;b&quot;</button>");
279
+ });
280
+
281
+ it("escapes XML in backgrounded event name", () => {
282
+ const result = buildEvent({
283
+ name: "test",
284
+ type: "user-message",
285
+ session: "main",
286
+ backgroundedEvent: 'task & "stuff"',
287
+ text: "hello",
288
+ });
289
+ expect(result).toContain('backgrounded-event name="task &amp; &quot;stuff&quot;"');
290
+ });
291
+ });
package/src/prompts.ts CHANGED
@@ -1,12 +1,3 @@
1
- export const MAIN_TIMEOUT = 60_000;
2
- export const CRON_TIMEOUT = 300_000;
3
- export const BG_TIMEOUT = 1_800_000;
4
-
5
- const fmtMin = (ms: number) => {
6
- const m = ms / 60_000;
7
- return `${m} minute${m > 1 ? "s" : ""}`;
8
- };
9
-
10
1
  export const SYSTEM_PROMPT = `\
11
2
  AI assistant running in macroclaw, an autonomous agent platform. \
12
3
  Persistent workspace at cwd with config, memory, skills. \
@@ -24,25 +15,150 @@ Use raw <b>, <i>, <code>, <pre> tags. Escape &, <, > in text content as &amp;, &
24
15
  Architecture: message bridge connecting chat interface and scheduled tasks. \
25
16
  Persistent session — conversation history carries across messages. Workspace persists across sessions.
26
17
 
27
- Context tags: messages may be prefixed with [Context: <type>]. Types:
28
- - cron/<name>automated scheduled task. Prefer action="silent" when nothing noteworthy.
29
- - button-clickuser tapped an inline keyboard button.
30
- - background-result/<name>output from a background agent you spawned. Decide whether to relay or handle silently.
31
- - background-agent/<name>you are a background agent. Complete task, return result. Cannot spawn sub-agents.
18
+ Event format: every incoming message is wrapped in an <event> XML block. Attributes:
19
+ - name — short identifier for this event (e.g. "check-logs", "cron-daily").
20
+ - typewhat triggered this event. One of:
21
+ - user-messagedirect user message. Content in <text>, optional <files>.
22
+ - button-clickuser tapped an inline button. Label in <button>.
23
+ - schedule-trigger — automated scheduled task. Contains <schedule> with name and optional missed-by/scheduled-at attributes. Prefer action="silent" when nothing noteworthy.
24
+ - background-agent-start — you are a background agent. Complete the task in <text> and return a result.
25
+ - background-agent-result — a background agent has finished. Contains <original-event name="..." /> linking to the agent that produced it, and a <result> block with <text> and optional <files>. Always use action="send" — the user expects to see the outcome. Summarize, relay, or add additional context from the conversation as appropriate.
26
+ - peek — status check on a running session. Contains <target-event name="..." /> identifying the event being peeked at. Only consider progress since that event started. Respond with a brief status update (2-3 sentences): what has been done, what's happening now, what's remaining. Return plain text, not structured output.
27
+ - session — "main" (primary conversation) or "background" (background agent).
28
+
29
+ Backgrounded events: when a new message arrives while a previous task is still running, \
30
+ the running task is automatically moved to a background session. The new event will contain \
31
+ a <backgrounded-event name="..." /> element referencing the task that was moved. \
32
+ This is informational — the backgrounded task continues running independently. \
33
+ Do not re-execute or act on the backgrounded task; focus on the new event's content.
34
+
35
+ Inner elements:
36
+ - <text> — the message text or task description.
37
+ - <files> — list of <file path="..." /> attachments. Read/view at those paths.
38
+ - <button> — the label of the tapped button.
39
+ - <schedule> — cron job metadata (name, missed-by, scheduled-at attributes).
40
+ - <backgrounded-event name="..." /> — a previously running task moved to background (see above).
41
+ - <original-event name="..." /> — in background-agent-result, links to the agent that produced the result.
42
+ - <target-event name="..." /> — in peek, identifies the event being checked on.
43
+ - <result> — wraps the output from a completed background agent. Contains <text> and optional <files>.
44
+ - <instructions> — inline guidance for how to handle this specific event. Always follow these instructions.
32
45
 
33
46
  Background agents: spawn alongside any response via backgroundAgents array:
34
47
  backgroundAgents: [{ name: "label", prompt: "task", model: "haiku" }]
35
- Each runs in same workspace, fresh session. Result fed back as [Context: background-result/<name>].
48
+ Each runs in same workspace, forked session. Result fed back as background-agent-result event.
36
49
  Models: haiku (fast/cheap), sonnet (balanced, default), opus (complex reasoning).
37
- User can spawn directly with "bg:" prefix. Use for long-running tasks that shouldn't block.
50
+ User can spawn directly with /bg command. Use for long-running tasks that shouldn't block.
38
51
 
39
- Files: attachments listed as [File: /path] prefixes. Read/view at those paths. \
40
- Send files via files array (absolute paths). Images (.png/.jpg/.jpeg/.gif/.webp) as photos, rest as documents. 50MB limit.
52
+ Session routing: if a new message arrives while your session is busy for over 1 minute, \
53
+ the running task is automatically moved to background and a new session is forked. \
54
+ The new event will contain a <backgrounded-event> element (see above).
41
55
 
42
- Timeouts: user=${fmtMin(MAIN_TIMEOUT)}, cron=${fmtMin(CRON_TIMEOUT)}, background=${fmtMin(BG_TIMEOUT)}. \
43
- On timeout, task continues in background automatically. Spawn background agents proactively for long tasks.
56
+ Files: send files via files array (absolute paths). \
57
+ Images (.png/.jpg/.jpeg/.gif/.webp) as photos, rest as documents. 50MB limit.
44
58
 
45
- Cron: jobs in data/schedule.json (hot-reloaded). Use "silent" when check finds nothing new, "send" when noteworthy.
59
+ Cron: jobs in data/schedule.json (hot-reloaded). Cron jobs always run as background sessions. \
60
+ Use "silent" when check finds nothing new, "send" when noteworthy.
46
61
 
47
62
  MessageButtons: include a buttons field (flat array of label strings) to attach inline buttons below your message. \
48
63
  Each button gets its own row. Max 27 characters per label — if options need more detail, describe them in the message and use short labels on buttons.`;
64
+
65
+ // --- Event builder ---
66
+
67
+ export function escapeXml(text: string): string {
68
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
69
+ }
70
+
71
+ export type SessionType = "main" | "background";
72
+
73
+ export type EventType =
74
+ | "user-message"
75
+ | "button-click"
76
+ | "schedule-trigger"
77
+ | "background-agent-start"
78
+ | "background-agent-result"
79
+ | "peek";
80
+
81
+ export interface EventInput {
82
+ name: string;
83
+ type: EventType;
84
+ session: SessionType;
85
+ text?: string;
86
+ files?: string[];
87
+ button?: string;
88
+ schedule?: { name: string; missedBy?: string; scheduledAt?: string };
89
+ backgroundedEvent?: string;
90
+ originalEvent?: string;
91
+ targetEvent?: string;
92
+ instructions?: string;
93
+ result?: { text: string; files?: string[] };
94
+ }
95
+
96
+ export function buildEvent(input: EventInput): string {
97
+ const lines: string[] = [
98
+ `<event name="${escapeXml(input.name)}" type="${input.type}" session="${input.session}">`,
99
+ ];
100
+
101
+ // Backgrounded event (always before content for visibility)
102
+ if (input.backgroundedEvent) {
103
+ lines.push(`<backgrounded-event name="${escapeXml(input.backgroundedEvent)}" />`);
104
+ }
105
+
106
+ // Schedule metadata
107
+ if (input.schedule) {
108
+ const attrs = [`name="${escapeXml(input.schedule.name)}"`];
109
+ if (input.schedule.missedBy) attrs.push(`missed-by="${escapeXml(input.schedule.missedBy)}"`);
110
+ if (input.schedule.scheduledAt) attrs.push(`scheduled-at="${escapeXml(input.schedule.scheduledAt)}"`);
111
+ lines.push(`<schedule ${attrs.join(" ")} />`);
112
+ }
113
+
114
+ // Original event reference (for background-agent-result)
115
+ if (input.originalEvent) {
116
+ lines.push(`<original-event name="${escapeXml(input.originalEvent)}" />`);
117
+ }
118
+
119
+ // Target event reference (for peek)
120
+ if (input.targetEvent) {
121
+ lines.push(`<target-event name="${escapeXml(input.targetEvent)}" />`);
122
+ }
123
+
124
+ // Result block (for background-agent-result)
125
+ if (input.result) {
126
+ lines.push("<result>");
127
+ lines.push(`<text>${escapeXml(input.result.text)}</text>`);
128
+ if (input.result.files?.length) {
129
+ lines.push("<files>");
130
+ for (const f of input.result.files) {
131
+ lines.push(` <file path="${escapeXml(f)}" />`);
132
+ }
133
+ lines.push("</files>");
134
+ }
135
+ lines.push("</result>");
136
+ }
137
+
138
+ // Button label
139
+ if (input.button) {
140
+ lines.push(`<button>${escapeXml(input.button)}</button>`);
141
+ }
142
+
143
+ // Text content
144
+ if (input.text) {
145
+ lines.push(`<text>${escapeXml(input.text)}</text>`);
146
+ }
147
+
148
+ // Files
149
+ if (input.files?.length) {
150
+ lines.push("<files>");
151
+ for (const f of input.files) {
152
+ lines.push(` <file path="${escapeXml(f)}" />`);
153
+ }
154
+ lines.push("</files>");
155
+ }
156
+
157
+ // Instructions (inline guidance for long sessions)
158
+ if (input.instructions) {
159
+ lines.push(`<instructions>${escapeXml(input.instructions)}</instructions>`);
160
+ }
161
+
162
+ lines.push("</event>");
163
+ return lines.join("\n");
164
+ }
@@ -17,7 +17,7 @@ function readScheduleConfig() {
17
17
  }
18
18
 
19
19
  function makeOnJob() {
20
- return mock((_name: string, _prompt: string, _model?: string) => {});
20
+ return mock((_name: string, _prompt: string, _model?: string, _missed?: { missedBy: string; scheduledAt: string }) => {});
21
21
  }
22
22
 
23
23
  // Build a cron expression that matches the current minute
@@ -169,6 +169,7 @@ describe("Scheduler — fireAt jobs", () => {
169
169
  s.stop();
170
170
 
171
171
  expect(onJob).toHaveBeenCalledWith("now", "do it", undefined);
172
+ expect(onJob.mock.calls[0][3]).toBeUndefined();
172
173
  });
173
174
 
174
175
  it("removes one-shot job after firing", () => {
@@ -220,9 +221,10 @@ describe("Scheduler — fireAt jobs", () => {
220
221
  expect(onJob).toHaveBeenCalledTimes(1);
221
222
  const call = onJob.mock.calls[0];
222
223
  expect(call[0]).toBe("reminder");
223
- expect(call[1]).toContain("[missed event, should have fired");
224
- expect(call[1]).toContain("min ago at");
225
- expect(call[1]).toContain("buy milk");
224
+ expect(call[1]).toBe("buy milk");
225
+ expect(call[3]).toBeDefined();
226
+ expect(call[3]!.missedBy).toMatch(/^\d+m$/);
227
+ expect(call[3]!.scheduledAt).toBeDefined();
226
228
  });
227
229
 
228
230
  it("removes missed one-shot job after firing", () => {
@@ -257,8 +259,9 @@ describe("Scheduler — fireAt jobs", () => {
257
259
  expect(onJob).toHaveBeenCalledTimes(1);
258
260
  const call = onJob.mock.calls[0];
259
261
  expect(call[0]).toBe("recent");
260
- expect(call[1]).toContain("[missed event, should have fired");
261
- expect(call[1]).toContain("still valid");
262
+ expect(call[1]).toBe("still valid");
263
+ expect(call[3]).toBeDefined();
264
+ expect(call[3]!.missedBy).toMatch(/^\d+m$/);
262
265
  });
263
266
 
264
267
  it("discards stale one-shot job (older than a week) without firing", () => {
package/src/scheduler.ts CHANGED
@@ -21,8 +21,13 @@ const scheduleConfigSchema = z.object({
21
21
  type ScheduleConfig = z.infer<typeof scheduleConfigSchema>;
22
22
  type Job = z.infer<typeof jobSchema>;
23
23
 
24
+ export interface MissedInfo {
25
+ missedBy: string;
26
+ scheduledAt: string;
27
+ }
28
+
24
29
  export interface SchedulerConfig {
25
- onJob: (name: string, prompt: string, model?: string) => void;
30
+ onJob: (name: string, prompt: string, model?: string, missed?: MissedInfo) => void;
26
31
  }
27
32
 
28
33
  const TICK_INTERVAL = 10_000; // 10 seconds
@@ -148,9 +153,11 @@ export class Scheduler {
148
153
 
149
154
  if (diff <= MAX_MISSED_MS) {
150
155
  const missedMinutes = Math.round(diff / 60_000);
151
- const missedPrompt = `[missed event, should have fired ${missedMinutes} min ago at ${job.fireAt}] ${job.prompt}`;
152
156
  log.info({ name: job.name, missedMinutes, fireAt: job.fireAt }, "Firing missed one-shot job");
153
- this.#config.onJob(job.name, missedPrompt, job.model);
157
+ this.#config.onJob(job.name, job.prompt, job.model, {
158
+ missedBy: `${missedMinutes}m`,
159
+ scheduledAt: job.fireAt,
160
+ });
154
161
  return "remove";
155
162
  }
156
163