@agentmessier/openclaw-agent-messier 0.3.10 → 0.3.12

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/index.ts CHANGED
@@ -3,7 +3,7 @@ import { memberTools, venuesTool, agentIdOf, identityOf, venueUrl, type PluginCf
3
3
  import { defaultVenueTools, defaultRealtimeVenue, joinVenue } from "./src/generate.js";
4
4
  import { startObserveWatcher } from "./src/watcher.js";
5
5
  import { session } from "./src/state.js";
6
- import { runAutoplayTurn, STRICT_JSON_DIRECTIVE } from "./src/decide.js";
6
+ import { runAutoplayTurn, postDecisionReport, STRICT_JSON_DIRECTIVE } from "./src/decide.js";
7
7
 
8
8
  /** A lobby row is "ours" if it references our agentId anywhere (soccer puts it in
9
9
  * sides.home/away; a generic venue may shape it differently). Deep, shape-blind. */
@@ -55,6 +55,10 @@ export default function register(api: OpenClawPluginApi) {
55
55
 
56
56
  let controller: AbortController | null = null;
57
57
  let poller: ReturnType<typeof setInterval> | null = null;
58
+ // The per-match session key currently in play. We persist ONE session per match
59
+ // (so the agent accumulates its own decision history); on LEAVING a match we
60
+ // delete it so a new match starts fresh and stale transcripts don't pile up.
61
+ let activeSessionKey: string | null = null;
58
62
 
59
63
  api.registerService({
60
64
  id: `agentnet-${venue.id}-watcher`,
@@ -75,11 +79,18 @@ export default function register(api: OpenClawPluginApi) {
75
79
 
76
80
  controller?.abort(); // leaving a previous room
77
81
  controller = new AbortController();
82
+ // Leaving the previous match → drop its persistent session so the new
83
+ // match starts with fresh context (and stale transcripts don't pile up).
84
+ if (sessionKey && activeSessionKey && activeSessionKey !== `${sessionKey}:${seat.id}`) {
85
+ api.runtime.subagent.deleteSession({ sessionKey: activeSessionKey }).catch(() => {});
86
+ }
87
+ // ONE persistent session for THIS match, stable across all its turns.
88
+ activeSessionKey = sessionKey ? `${sessionKey}:${seat.id}` : null;
78
89
  let move = 0;
79
90
  // Fire-and-forget: the watcher runs until aborted or the gateway stops.
80
91
  void startObserveWatcher(
81
92
  { serverUrl: cfg.serverUrl, matchId: seat.id!, agentId, mode: cfg.mode, strategyFile: cfg.strategyFile, actTool, label },
82
- async (msg) => {
93
+ async ({ system, user, tick, clock }) => {
83
94
  if (!sessionKey) {
84
95
  ctx.logger.warn(`[${label}] no sessionKey configured; cannot deliver move prompts.`);
85
96
  return;
@@ -90,30 +101,58 @@ export default function register(api: OpenClawPluginApi) {
90
101
  // 2-decisions-in-157s). soccer_play stays registered for interactive
91
102
  // chat play; only AUTOPLAY changes to this server-driven loop.
92
103
  //
93
- // Fresh session per move: each prompt is a complete snapshot, so the
94
- // agent needs no history keeps context from overflowing.
104
+ // ONE persistent session per MATCH (sessionKey:matchId, stable across
105
+ // turns) so the agent accumulates contextits own prior decisions —
106
+ // and the session transcript is the full per-match request log. `turn`
107
+ // is kept only for the idempotency key and the context-growth cap.
95
108
  const turn = move++;
109
+ const matchSessionKey = `${sessionKey}:${seat.id}`;
96
110
  const idempotencyKey = `agentnet:${venue.id}:${seat.id}:${agentId}:${turn}`;
111
+ const did = session.did ?? agentId;
112
+ const sys = system || undefined;
113
+ const msg = `${user}${STRICT_JSON_DIRECTIVE}`;
97
114
  // Mark when this prompt was handed to the agent: x-agent-decision-ms is
98
115
  // the prompt→reply latency measured inside runAutoplayTurn.
99
116
  session.promptDeliveredAt = Date.now();
100
117
  const result = await runAutoplayTurn({
101
118
  runtime: api.runtime,
102
- sessionKey: `${sessionKey}:${turn}`,
119
+ sessionKey: matchSessionKey,
120
+ turn,
103
121
  idempotencyKey,
122
+ // The static rulebook (spec.instructions.system) rides the SYSTEM
123
+ // channel; the per-tick board is the user message. '' on the
124
+ // fallback path → no extra system prompt.
125
+ extraSystemPrompt: sys,
104
126
  // Steer the agent to reply with ONLY the moves JSON (no tool call).
105
- message: `${msg}${STRICT_JSON_DIRECTIVE}`,
127
+ message: msg,
106
128
  // 45s ceiling, matching the watcher's per-delivery watchdog backstop:
107
129
  // a run that hasn't produced a decision by then is treated as stalled
108
130
  // (was 300s, which let one hung run silence the team for 5 min — m171).
109
131
  timeoutMs: 45_000,
110
132
  matchId: seat.id!,
111
133
  cfg,
112
- did: session.did ?? agentId,
134
+ did,
113
135
  token: session.token,
114
136
  base,
115
137
  logger: ctx.logger,
116
138
  });
139
+ // Report EVERY decision to the pitch — acted AND no-response — so the
140
+ // decision inspector sees no-response turns too. Best-effort, off the
141
+ // hot path, never throws.
142
+ void postDecisionReport(
143
+ {
144
+ tick,
145
+ clock,
146
+ prompt: { ...(sys ? { system: sys } : {}), user: msg },
147
+ outcome: result.outcome,
148
+ ...(result.reason ? { reason: result.reason } : {}),
149
+ ...(result.moves ? { moves: result.moves } : {}),
150
+ rawText: result.rawText,
151
+ latencyMs: result.latencyMs,
152
+ ...(session.lastModel ? { model: session.lastModel } : {}),
153
+ },
154
+ { base, matchId: seat.id!, agentId: did, token: session.token, logger: ctx.logger },
155
+ );
117
156
  // act-verification by the natural signal: did we parse+post (or did the
118
157
  // model act via the tool)? A parse-miss = "responded without acting" —
119
158
  // log, keep standing orders, continue (never freeze). The watcher's own
@@ -182,6 +221,10 @@ export default function register(api: OpenClawPluginApi) {
182
221
  if (poller) { clearInterval(poller); poller = null; }
183
222
  controller?.abort();
184
223
  controller = null;
224
+ if (activeSessionKey) {
225
+ api.runtime.subagent.deleteSession({ sessionKey: activeSessionKey }).catch(() => {});
226
+ activeSessionKey = null;
227
+ }
185
228
  session.joinAndWatch = null;
186
229
  session.matchId = null;
187
230
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentmessier/openclaw-agent-messier",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "description": "Agent Messier multi-venue client for OpenClaw \u2014 play games and work tasks on the AgentNet platform (soccer today; venues discovered from the marketplace registry)",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/decide.ts CHANGED
@@ -35,7 +35,9 @@ export type Move = {
35
35
  type: string;
36
36
  dir?: Vec2;
37
37
  power?: number;
38
- zone?: number;
38
+ /** Zone is now a NAME (e.g. "att-left"); an integer is accepted for back-compat
39
+ * and forwarded as-is (the server normalizes name→id). */
40
+ zone?: string | number;
39
41
  say?: string;
40
42
  };
41
43
 
@@ -60,10 +62,11 @@ export class DecideError extends Error {
60
62
  export const STRICT_JSON_DIRECTIVE =
61
63
  `\n\nDo NOT call any tool. Reply with ONLY a JSON object — no prose, no markdown ` +
62
64
  `fences — in exactly this shape, one entry per player you control:\n` +
63
- `{"moves":[{"playerId":"<id>","type":"<action>","dir":{"x":1,"y":0},"power":0.8,"zone":11,"say":"optional"}]}\n` +
65
+ `{"moves":[{"playerId":"<id>","type":"<action>","dir":{"x":1,"y":0},"power":0.8,"zone":"att-left","say":"optional"}]}\n` +
64
66
  `Valid action types: ${ACTION_TYPES.join(", ")}. ` +
65
- `"run"/"kick" need dir {x,y}; "kick" also needs power 0..1; "push" needs zone 1..12; ` +
66
- `"press"/"cover" may add zone 1..12. Omit fields an action does not need. Return the moves JSON now.`;
67
+ `"run"/"kick" need dir {x,y}; "kick" also needs power 0..1; "push" needs a zone NAME; ` +
68
+ `"press"/"cover" may add a zone NAME. Use the zone names from the board. ` +
69
+ `Omit fields an action does not need. Return the moves JSON now.`;
67
70
 
68
71
  // ── tolerant JSON extraction (ported from driver.ts) ─────────────────────────
69
72
 
@@ -167,7 +170,11 @@ export function parseMoves(text: string): Move[] {
167
170
  const dir = coerceDir(r.dir);
168
171
  if (dir) move.dir = dir;
169
172
  if (typeof r.power === "number" && Number.isFinite(r.power)) move.power = r.power;
170
- if (Number.isInteger(r.zone) && (r.zone as number) >= 1 && (r.zone as number) <= 12) move.zone = r.zone as number;
173
+ // Zones are now NAMES (e.g. "att-left"); accept a non-empty string and pass
174
+ // it through verbatim (the server is the authority — no client-side name
175
+ // validation). Still accept an integer for back-compat. Anything else drops.
176
+ if (typeof r.zone === "string" && r.zone.trim() !== "") move.zone = r.zone;
177
+ else if (Number.isInteger(r.zone)) move.zone = r.zone as number;
171
178
  if (typeof r.say === "string") move.say = r.say;
172
179
  moves.push(move);
173
180
  }
@@ -196,8 +203,10 @@ function blockText(content: unknown): string {
196
203
  }
197
204
 
198
205
  /** The text of the LAST assistant message in a getSessionMessages transcript.
199
- * A fresh per-turn session yields one user + one assistant record, so this is
200
- * trivially that assistant reply. Returns "" when there is no assistant text. */
206
+ * With a PERSISTENT per-match session the transcript GROWS across turns
207
+ * (user/assistant pairs accumulate), so we scan from the end and return the
208
+ * most recent assistant reply — never an earlier turn's. Returns "" when there
209
+ * is no assistant text. */
201
210
  export function lastAssistantText(messages: unknown[]): string {
202
211
  for (let i = messages.length - 1; i >= 0; i -= 1) {
203
212
  const m = messages[i];
@@ -263,14 +272,76 @@ export async function executeMoves(
263
272
  return { posted: results.length, results };
264
273
  }
265
274
 
275
+ // ── decision reporting: POST every turn's decision to the pitch ──────────────
276
+
277
+ export type DecisionReportDeps = {
278
+ base: string;
279
+ matchId: string;
280
+ /** Reporting agentId (session.did ?? agentId). */
281
+ agentId: string;
282
+ /** Seat token — sent as x-agent-token (same auth as executeMoves). */
283
+ token?: string | null | undefined;
284
+ fetch?: typeof fetch;
285
+ logger?: { warn: (m: string) => void };
286
+ };
287
+
288
+ export type DecisionReport = {
289
+ tick: number;
290
+ clock: number;
291
+ prompt: { system?: string; user: string };
292
+ outcome: "acted" | "no_response";
293
+ reason?: string;
294
+ moves?: Move[];
295
+ rawText?: string;
296
+ latencyMs?: number;
297
+ model?: string;
298
+ };
299
+
300
+ /**
301
+ * POST a decision report to `/matches/:id/agents/:agentId/decision` so EVERY
302
+ * turn — acted AND no-response — reaches the pitch's decision inspector. Auth is
303
+ * the seat token (x-agent-token), same as executeMoves. Best-effort and off the
304
+ * hot path: it NEVER throws (caller fires it `.catch`-free via this swallow) so a
305
+ * reporting failure can't disrupt play.
306
+ */
307
+ export async function postDecisionReport(report: DecisionReport, deps: DecisionReportDeps): Promise<void> {
308
+ const f = deps.fetch ?? fetch;
309
+ const base = deps.base.replace(/\/$/, "");
310
+ const url = `${base}/matches/${encodeURIComponent(deps.matchId)}/agents/${encodeURIComponent(deps.agentId)}/decision`;
311
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
312
+ if (deps.token) headers["x-agent-token"] = deps.token;
313
+ try {
314
+ await f(url, { method: "POST", headers, body: JSON.stringify(report) });
315
+ } catch (e) {
316
+ deps.logger?.warn(`decision report POST failed: ${String(e)}`);
317
+ }
318
+ }
319
+
266
320
  // ── the full autoplay turn: run → wait → read → parse → post ─────────────────
267
321
 
322
+ /** Context-growth safety cap. A persistent per-match session accumulates the
323
+ * agent's whole decision history (good: it remembers its own prior orders), but
324
+ * a long match would otherwise grow the transcript without bound. OpenClaw
325
+ * auto-compacts, but as a hard backstop we RESET the session (deleteSession +
326
+ * start fresh) once a single match exceeds this many turns. Tradeoff: the agent
327
+ * loses its accumulated in-match memory at the reset boundary, but context can
328
+ * never grow unbounded. Keep it generous enough that most matches never hit it. */
329
+ export const SESSION_RESET_TURN_CAP = 60;
330
+
268
331
  export type AutoplayTurnDeps = {
269
332
  runtime: PluginRuntime;
333
+ /** Stable PER-MATCH session key — persists across turns so the agent
334
+ * accumulates context (its own prior decisions) and the transcript is the
335
+ * full per-match request log. Reset only on leave/new-match/cap. */
270
336
  sessionKey: string;
337
+ /** This match's turn ordinal (0-based). Used for the reset cap only. */
338
+ turn: number;
271
339
  idempotencyKey: string;
272
- /** The strict-JSON move prompt to deliver to the agent. */
340
+ /** The strict-JSON move prompt (the per-tick board) to deliver to the agent. */
273
341
  message: string;
342
+ /** The static game rulebook (spec.instructions.system), delivered on the run's
343
+ * SYSTEM channel rather than concatenated into the per-tick board. */
344
+ extraSystemPrompt?: string;
274
345
  /** Backstop ceiling for waitForRun (the watcher watchdog is the latch backstop). */
275
346
  timeoutMs: number;
276
347
  matchId: string;
@@ -281,10 +352,29 @@ export type AutoplayTurnDeps = {
281
352
  logger?: { info: (m: string) => void; warn: (m: string) => void; error: (m: string) => void };
282
353
  };
283
354
 
284
- export type AutoplayTurnResult =
285
- | { acted: true; via: "post"; posted: number }
286
- | { acted: true; via: "tool" } // the model called soccer_play itself
287
- | { acted: false; reason: string }; // responded without acting (parse miss / empty reply)
355
+ /** Everything a decision REPORT needs, returned alongside the act outcome so the
356
+ * caller (index.ts) can POST one /decision report per turn — for acted AND
357
+ * no-response turns using the tick/clock it has from the delivered frame. */
358
+ export type AutoplayTurnObservability = {
359
+ /** "acted" when the team was moved this cycle (direct post OR the model's own
360
+ * tool call); "no_response" when the run finished without a usable decision. */
361
+ outcome: "acted" | "no_response";
362
+ /** No-act reason (parse miss / empty reply); undefined when acted. */
363
+ reason?: string;
364
+ /** The agent's raw reply text (last assistant message), "" if unreadable. */
365
+ rawText: string;
366
+ /** Parsed moves (present only when we parsed + posted; undefined on tool/miss). */
367
+ moves?: Move[];
368
+ /** Measured prompt→reply latency (ms). */
369
+ latencyMs: number;
370
+ };
371
+
372
+ export type AutoplayTurnResult = AutoplayTurnObservability &
373
+ (
374
+ | { acted: true; via: "post"; posted: number }
375
+ | { acted: true; via: "tool" } // the model called soccer_play itself
376
+ | { acted: false } // responded without acting (parse miss / empty reply)
377
+ );
288
378
 
289
379
  /**
290
380
  * Force exactly one agent turn for a delivered situation and act on its reply.
@@ -296,11 +386,21 @@ export async function runAutoplayTurn(deps: AutoplayTurnDeps): Promise<AutoplayT
296
386
  // act-verification baseline: if soccer_play runs during this turn it advances
297
387
  // session.lastActAt past this — our cue to NOT double-post.
298
388
  const actAtBefore = session.lastActAt;
389
+
390
+ // Context-growth backstop: once a single match's persistent session exceeds the
391
+ // cap, drop it so the next run starts fresh — bounds transcript size at the
392
+ // cost of the agent's accumulated in-match memory (see SESSION_RESET_TURN_CAP).
393
+ if (deps.turn > 0 && deps.turn % SESSION_RESET_TURN_CAP === 0) {
394
+ await runtime.subagent.deleteSession({ sessionKey: deps.sessionKey }).catch(() => {});
395
+ deps.logger?.info(`session reset at turn ${deps.turn} (cap ${SESSION_RESET_TURN_CAP}) — context bounded`);
396
+ }
397
+
299
398
  const startedAt = Date.now();
300
399
 
301
400
  const { runId } = await runtime.subagent.run({
302
401
  sessionKey: deps.sessionKey,
303
402
  message: deps.message,
403
+ ...(deps.extraSystemPrompt ? { extraSystemPrompt: deps.extraSystemPrompt } : {}),
304
404
  deliver: false,
305
405
  idempotencyKey: deps.idempotencyKey,
306
406
  });
@@ -308,28 +408,36 @@ export async function runAutoplayTurn(deps: AutoplayTurnDeps): Promise<AutoplayT
308
408
  const decisionMs = Math.max(0, Date.now() - startedAt);
309
409
 
310
410
  // The model called the act tool itself (it POSTed + stamped lastActAt). Treat
311
- // the cycle as acted; do NOT parse+post again (would double-apply).
411
+ // the cycle as acted; do NOT parse+post again (would double-apply). The
412
+ // session PERSISTS — no deleteSession here; it's the per-match request log.
312
413
  if (session.lastActAt !== actAtBefore) {
313
- return { acted: true, via: "tool" };
414
+ return { acted: true, via: "tool", outcome: "acted", rawText: "", latencyMs: decisionMs };
314
415
  }
315
416
 
316
417
  let text = "";
317
418
  try {
419
+ // The persistent transcript GROWS each turn; we read the most recent slice
420
+ // and lastAssistantText returns the LATEST assistant reply (not an old one).
318
421
  const { messages } = await runtime.subagent.getSessionMessages({ sessionKey: deps.sessionKey, limit: 10 });
319
422
  text = lastAssistantText(messages);
320
423
  } catch (e) {
321
424
  deps.logger?.warn(`getSessionMessages failed: ${String(e)}`);
322
- } finally {
323
- // Fresh session per turn → drop it so transcripts don't accumulate. Cleanup
324
- // failure is non-fatal (the run already happened).
325
- runtime.subagent.deleteSession({ sessionKey: deps.sessionKey }).catch(() => {});
326
425
  }
426
+ // NOTE: no per-turn deleteSession — the session persists for the whole match so
427
+ // the agent accumulates its own decision history. Reset happens only on
428
+ // leave/new-match (index.ts) or the turn cap above.
327
429
 
328
430
  let moves: Move[];
329
431
  try {
330
432
  moves = parseMoves(text);
331
433
  } catch (e) {
332
- return { acted: false, reason: e instanceof DecideError ? e.message : String(e) };
434
+ return {
435
+ acted: false,
436
+ outcome: "no_response",
437
+ reason: e instanceof DecideError ? e.message : String(e),
438
+ rawText: text,
439
+ latencyMs: decisionMs,
440
+ };
333
441
  }
334
442
 
335
443
  const { posted } = await executeMoves(deps.matchId, moves, {
@@ -339,5 +447,5 @@ export async function runAutoplayTurn(deps: AutoplayTurnDeps): Promise<AutoplayT
339
447
  token: deps.token,
340
448
  decisionMs,
341
449
  });
342
- return { acted: true, via: "post", posted };
450
+ return { acted: true, via: "post", posted, outcome: "acted", rawText: text, moves, latencyMs: decisionMs };
343
451
  }
package/src/watcher.ts CHANGED
@@ -100,36 +100,52 @@ export function parseSseBlock(block: string): { event?: string; data?: string }
100
100
  return event !== undefined ? { event, ...(data !== undefined ? { data } : {}) } : (data !== undefined ? { data } : {});
101
101
  }
102
102
 
103
- export function prompt(v: TeamView & { summary?: string }, mode: "easy" | "advanced" | "both", strategyFile?: string, spec?: GameSpec | null, actTool = "soccer_play"): string {
103
+ /** A delivered turn, split so the rulebook can ride the model's SYSTEM channel.
104
+ * - `system`: the STATIC game rules (spec.instructions.system) — passed as
105
+ * subagent.run's extraSystemPrompt. Empty on the fallback path (no spec).
106
+ * - `user`: the PER-TICK board — strategy + rendered situation + play guidance
107
+ * + the act/JSON directive. This is what changes every tick.
108
+ * - `tick`/`clock`: the delivered frame's game time, threaded through so the
109
+ * deliver callback can stamp the per-turn decision report. */
110
+ export type DeliverPrompt = { system: string; user: string; tick: number; clock: number };
111
+
112
+ const actDirective = (actTool: string) =>
113
+ `Decide and act NOW: make ONE ${actTool} call with a move for every player you control — ` +
114
+ `every time you are prompted, even if the plan is unchanged. The order holds until you change it, ` +
115
+ `so if you go quiet your team freezes on stale orders. Never reply without acting.`;
116
+
117
+ export function prompt(v: TeamView & { summary?: string }, mode: "easy" | "advanced" | "both", strategyFile?: string, spec?: GameSpec | null, actTool = "soccer_play"): DeliverPrompt {
104
118
  const standing = strategyText(strategyFile);
105
119
  const stratBlock = standing ? `## Your manager's standing instructions\n${standing}\n\n` : "";
106
120
  const ins = spec?.instructions;
107
121
  if (ins && ins.system && ins.play && v.summary) {
108
122
  // Generic GSP path: the server authored the instructions AND rendered the
109
- // situation the plugin only concatenates. Tool-calling host, so the
110
- // direct-JSON `output` contract is replaced by a generic tool-act line
111
- // (host concern, not game knowledge).
112
- return (
113
- `${ins.system}\n\n` +
114
- stratBlock +
115
- `${v.summary}\n\n` +
116
- `${ins.play}\n\n` +
117
- `Decide and act NOW: make ONE ${actTool} call with a move for every player you control — ` +
118
- `every time you are prompted, even if the plan is unchanged. The order holds until you change it, ` +
119
- `so if you go quiet your team freezes on stale orders. Never reply without acting.`
120
- );
123
+ // situation. The rulebook (static) rides the SYSTEM channel; the board
124
+ // (per-tick) is the user message. Tool-calling host, so the direct-JSON
125
+ // `output` contract is replaced by a generic tool-act line (host concern,
126
+ // not game knowledge).
127
+ return {
128
+ system: ins.system,
129
+ user:
130
+ stratBlock +
131
+ `${v.summary}\n\n` +
132
+ `${ins.play}\n\n` +
133
+ actDirective(actTool),
134
+ tick: v.tick,
135
+ clock: v.clock,
136
+ };
121
137
  }
122
138
  // Fallback (pre-envelope server or handshake not yet arrived). describeTeam is
123
139
  // the soccer-specific renderer — only valid for a TeamView; for any other
124
140
  // venue with no server instructions, dump the raw view rather than crash.
141
+ // No server-authored rulebook here, so the system channel is empty.
125
142
  const rendered = Array.isArray(v.mine) ? describeTeam(v, mode) : JSON.stringify(v);
126
- return (
127
- stratBlock +
128
- `${rendered}\n\n` +
129
- `Decide and act NOW: make ONE ${actTool} call with a move for every player you control — ` +
130
- `every time you are prompted, even if the plan is unchanged. The order holds until you change it, ` +
131
- `so if you go quiet your team freezes on stale orders. Never reply without acting.`
132
- );
143
+ return {
144
+ system: "",
145
+ user: stratBlock + `${rendered}\n\n` + actDirective(actTool),
146
+ tick: v.tick,
147
+ clock: v.clock,
148
+ };
133
149
  }
134
150
 
135
151
  /** The observe path for a venue, from spec.routes ({matchId}/{did} substituted),
@@ -143,7 +159,7 @@ export function observeUrl(spec: GameSpec | null, matchId: string, did: string):
143
159
 
144
160
  export async function startObserveWatcher(
145
161
  cfg: WatcherCfg,
146
- deliver: (msg: string) => void | Promise<void>,
162
+ deliver: (p: DeliverPrompt) => void | Promise<void>,
147
163
  options: WatcherOptions = {},
148
164
  ): Promise<void> {
149
165
  const { signal, logger } = options;