@maolon/pi-watcher 0.1.3 → 0.1.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/CHANGELOG.md CHANGED
@@ -6,6 +6,11 @@ All notable changes to this project are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.4] - 2026-10-08
10
+
11
+ ### Added
12
+ - `/watcher cancel [reason]` closes every active or paused watch of the current session, no watchId needed. Each close withdraws pending attentions and supersedes open episodes like a per-watch `close`; other sessions are untouched.
13
+
9
14
  ## [0.1.3] - 2026-10-08
10
15
 
11
16
  ### Changed
package/README.md CHANGED
@@ -146,7 +146,8 @@ degraded; they never mark the task as failed. The command text is stored as evid
146
146
  | `register` | Full spec: `run`, `group` (up to 16 members) and `obligation` targets |
147
147
 
148
148
  `/watcher` shows the panel; `/watcher ack <episodeId> <received|investigating|defer|resolved|dismiss> [until]`
149
- handles an episode locally; `/watcher jev` manages the optional semantic review (see below).
149
+ handles an episode locally; `/watcher cancel [reason]` closes every active or paused watch of the current session
150
+ (no watchId needed); `/watcher jev` manages the optional semantic review (see below).
150
151
 
151
152
  ### Exit markers for `exec_command`
152
153
 
@@ -42,6 +42,13 @@ export declare class WatchService {
42
42
  inspectionId: Id;
43
43
  }>;
44
44
  control(requestId: Id, watchId: Id, expectedControlRevision: number, action: 'pause' | 'close' | 'resume', reason: string, actor: ActorContext): Promise<Json>;
45
+ /**
46
+ * Local owner cancel (for the /watcher command only): close every active or paused watch of this owner
47
+ * session. Each close goes through control() with the watch's current controlRevision, so the scope
48
+ * advance, attention withdraw and episode supersede are identical to a per-watch close. A close that
49
+ * fails is reported per watch, never folded into success.
50
+ */
51
+ closeAllForOwner(reason: string, actor: ActorContext): Promise<Json>;
45
52
  /**
46
53
  * Layered withdrawal report (design 6.5): sourceFence for the scope advance and one entry
47
54
  * per affected route. "withdrawn" is claimed only when every route reports prevented
@@ -280,6 +280,35 @@ export class WatchService {
280
280
  }
281
281
  return { ...result, ...this.relayCutReport(result) };
282
282
  }
283
+ /**
284
+ * Local owner cancel (for the /watcher command only): close every active or paused watch of this owner
285
+ * session. Each close goes through control() with the watch's current controlRevision, so the scope
286
+ * advance, attention withdraw and episode supersede are identical to a per-watch close. A close that
287
+ * fails is reported per watch, never folded into success.
288
+ */
289
+ async closeAllForOwner(reason, actor) {
290
+ const rows = [];
291
+ let cursor;
292
+ for (;;) {
293
+ const page = this.store.transaction(tx => tx.listWatches(actor.owner.sessionId, cursor, 200, ['active', 'paused']));
294
+ rows.push(...page);
295
+ if (page.length < 200)
296
+ break;
297
+ cursor = page[page.length - 1].watchId;
298
+ }
299
+ const closed = [];
300
+ const failed = [];
301
+ for (const row of rows) {
302
+ try {
303
+ const r = await this.control(newId('cancel'), row.watchId, row.controlRevision, 'close', reason, actor);
304
+ closed.push({ watchId: row.watchId, previousLifecycle: row.lifecycle, withdrawn: Array.isArray(r.withdrawEventIds) ? r.withdrawEventIds.length : 0 });
305
+ }
306
+ catch (e) {
307
+ failed.push({ watchId: row.watchId, code: e.code ?? 'ERROR', message: e instanceof Error ? e.message : String(e) });
308
+ }
309
+ }
310
+ return { closed, failed, reason };
311
+ }
283
312
  /**
284
313
  * Layered withdrawal report (design 6.5): sourceFence for the scope advance and one entry
285
314
  * per affected route. "withdrawn" is claimed only when every route reports prevented
@@ -130,6 +130,9 @@ class SessionBackend {
130
130
  async localAck(req, actor) {
131
131
  return JSON.stringify(await toServiceResultAsync(req.requestId, () => this.rt.service.ackEpisode(req.requestId, req.episodeId, req.action, req.note, req.until, actor)));
132
132
  }
133
+ async localCancelAll(reason, actor) {
134
+ return JSON.stringify(await toServiceResultAsync(undefined, () => this.rt.service.closeAllForOwner(reason, actor)));
135
+ }
133
136
  requestWidgetRefresh() {
134
137
  this.refreshWidget();
135
138
  }
@@ -553,7 +556,7 @@ export default function watcherExtension(pi, options = {}) {
553
556
  ].join('\n');
554
557
  };
555
558
  pi.registerCommand('watcher', {
556
- description: 'pi-watcher panel: watches, health, open issues, and why no attention was sent. /watcher ack <episodeId> <received|investigating|defer|resolved|dismiss> [until ISO] — local owner response. /watcher jev [status] | /watcher jev consent <on|off> — optional Jev semantic review',
559
+ description: 'pi-watcher panel: watches, health, open issues, and why no attention was sent. /watcher ack <episodeId> <received|investigating|defer|resolved|dismiss> [until ISO] — local owner response. /watcher cancel [reason...] — close every active/paused watch of this session. /watcher jev [status] | /watcher jev consent <on|off> — optional Jev semantic review',
557
560
  handler: async (args, ctx) => {
558
561
  await ensureBackend(ctx);
559
562
  const be = backend;
@@ -597,6 +600,24 @@ export default function watcherExtension(pi, options = {}) {
597
600
  ctx.ui?.notify?.(result, parsed.ok === false ? 'error' : 'info');
598
601
  return;
599
602
  }
603
+ if (argv[0] === 'cancel') {
604
+ // Local owner cancel (non-model path): close every active/paused watch of this session, no watchId needed
605
+ const reason = argv.slice(1).join(' ') || 'local owner cancel';
606
+ const result = await be.localCancelAll(reason, actorOf(ctx));
607
+ be.requestWidgetRefresh();
608
+ const parsed = JSON.parse(result);
609
+ if (parsed.ok === false) {
610
+ ctx.ui?.notify?.(result, 'error');
611
+ return;
612
+ }
613
+ const closed = parsed.value?.closed?.length ?? 0;
614
+ const failed = parsed.value?.failed?.length ?? 0;
615
+ const head = closed === 0 && failed === 0
616
+ ? 'pi-watcher: no active or paused watch in this session'
617
+ : `pi-watcher: closed ${closed} watch(es) in this session` + (failed > 0 ? `, ${failed} failed` : '');
618
+ ctx.ui?.notify?.(failed > 0 ? `${head}\n${result}` : head, failed > 0 ? 'warning' : 'info');
619
+ return;
620
+ }
600
621
  const panel = await be.panel(actorOf(ctx));
601
622
  const text = JSON.stringify(panel, null, 2);
602
623
  ctx.ui?.notify?.(text.length > 4000 ? `${text.slice(0, 4000)}…` : text, 'info');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maolon/pi-watcher",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Watch long-running Pi tasks without babysitting: deterministic task monitoring, optional semantic review, and relay-delivered wakes only when something needs the host.",
5
5
  "keywords": [
6
6
  "pi-package",