@maolon/pi-watcher 0.1.2 → 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 +13 -0
- package/README.md +2 -1
- package/dist/engine/engine.d.ts +2 -0
- package/dist/engine/engine.js +4 -3
- package/dist/engine/service.d.ts +7 -0
- package/dist/engine/service.js +29 -0
- package/dist/pi-extension.d.ts +12 -0
- package/dist/pi-extension.js +34 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to this project are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
|
|
5
5
|
[Semantic Versioning](https://semver.org/). Before 1.0, minor versions may break.
|
|
6
6
|
|
|
7
|
+
## [Unreleased]
|
|
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
|
+
|
|
14
|
+
## [0.1.3] - 2026-10-08
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Attention toasts follow the outcome: a succeeded `task.terminal` notice shows at `info`; failures, cancellations, unknown exits, deadlines, semantic candidates and failed relay wakes stay at `warning`.
|
|
18
|
+
- Git flow: `dev` integration branch, release and hotfix PRs into `main` publish to npm.
|
|
19
|
+
|
|
7
20
|
## [0.1.2] - 2026-10-08
|
|
8
21
|
|
|
9
22
|
### 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
|
|
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
|
|
package/dist/engine/engine.d.ts
CHANGED
|
@@ -42,6 +42,8 @@ export interface AttentionNotice {
|
|
|
42
42
|
relayError?: string;
|
|
43
43
|
/** Deadline until which the envelope is still valid (ms epoch) */
|
|
44
44
|
validUntil: number;
|
|
45
|
+
/** Observed task state for terminal-fact notices (display layer picks the toast level from it) */
|
|
46
|
+
taskState?: string;
|
|
45
47
|
}
|
|
46
48
|
/** Accepted-judgment-complete notification (display-layer notice; judge review must be visible) */
|
|
47
49
|
export interface JudgmentNotice {
|
package/dist/engine/engine.js
CHANGED
|
@@ -236,7 +236,7 @@ export class WatchEngine {
|
|
|
236
236
|
}, now);
|
|
237
237
|
if (episodeId) {
|
|
238
238
|
produced.episodes.push(episodeId);
|
|
239
|
-
await this.publishEpisodeAttention(watchId, row, episodeId, kind, `${snapshot.taskState}${snapshot.exitCode !== undefined && snapshot.exitCode !== null ? ` exitCode=${snapshot.exitCode}` : ''}: ${snapshot.summary ?? 'no summary'}`, now);
|
|
239
|
+
await this.publishEpisodeAttention(watchId, row, episodeId, kind, `${snapshot.taskState}${snapshot.exitCode !== undefined && snapshot.exitCode !== null ? ` exitCode=${snapshot.exitCode}` : ''}: ${snapshot.summary ?? 'no summary'}`, now, undefined, snapshot.taskState);
|
|
240
240
|
}
|
|
241
241
|
const resultId = `result-${watchId}-${row.generation}-${snapshot.lastSourceSeq ?? 0}`;
|
|
242
242
|
const card = buildResultCard({
|
|
@@ -720,7 +720,7 @@ export class WatchEngine {
|
|
|
720
720
|
return { inspectionId, watchId, ingested: 0, produced, health: missing.length > 0 ? 'degraded' : 'healthy', lifecycleAfter: 'active' };
|
|
721
721
|
}
|
|
722
722
|
/** Unified attention publishing for hard facts / semantic candidates: truth lands in the outbox first, then publishes via relay managed delivery (I6). */
|
|
723
|
-
async publishEpisodeAttention(watchId, row, episodeId, reasonCode, summary, now, probability) {
|
|
723
|
+
async publishEpisodeAttention(watchId, row, episodeId, reasonCode, summary, now, probability, taskState) {
|
|
724
724
|
const envelopeId = newId('att');
|
|
725
725
|
const envelope = {
|
|
726
726
|
schemaVersion: 1,
|
|
@@ -785,7 +785,8 @@ export class WatchEngine {
|
|
|
785
785
|
summary,
|
|
786
786
|
transport,
|
|
787
787
|
relayError,
|
|
788
|
-
validUntil: validUntilMs
|
|
788
|
+
validUntil: validUntilMs,
|
|
789
|
+
taskState
|
|
789
790
|
});
|
|
790
791
|
}
|
|
791
792
|
catch { /* display-layer failure does not affect the engine */ }
|
package/dist/engine/service.d.ts
CHANGED
|
@@ -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
|
package/dist/engine/service.js
CHANGED
|
@@ -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
|
package/dist/pi-extension.d.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* source/profile/owner capability are injected from the trusted context; the model cannot specify sessionId
|
|
16
16
|
* - ack requires relay delivery (deliveryRef); when relay is not negotiated it returns NO_DELIVERY and never fabricates a receipt
|
|
17
17
|
*/
|
|
18
|
+
import { type AttentionNotice } from './engine/engine.js';
|
|
18
19
|
import { type PiClassifierRegistry } from './jev/pi-registry.js';
|
|
19
20
|
/** Minimal structural types: avoid a hard dependency on the pi runtime (the loader is Pi itself). */
|
|
20
21
|
export interface PiToolCallContext {
|
|
@@ -120,4 +121,15 @@ export interface RelayBindResult {
|
|
|
120
121
|
message?: string;
|
|
121
122
|
};
|
|
122
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Session backend: local runtime, no IPC (per-session root, decision 2026-09-21).
|
|
126
|
+
* This session is the only process on its own root, so the single-writer invariant holds naturally; no primary/attached,
|
|
127
|
+
* no failover; version skew (an old primary serving an old action surface) is structurally impossible.
|
|
128
|
+
*/
|
|
129
|
+
/**
|
|
130
|
+
* Toast level for an attention notice. A clean success is informational; anything that needs a
|
|
131
|
+
* closer look (failure, cancellation, unknown exit, deadline, semantic candidate) or a broken
|
|
132
|
+
* relay wake leg stays a warning.
|
|
133
|
+
*/
|
|
134
|
+
export declare function attentionNotifyLevel(notice: Pick<AttentionNotice, 'reasonCode' | 'transport' | 'taskState'>): 'info' | 'warning';
|
|
123
135
|
export default function watcherExtension(pi: PiExtensionAPI, options?: WatcherExtensionOptions): void;
|
package/dist/pi-extension.js
CHANGED
|
@@ -78,6 +78,16 @@ const sessionDirOf = (sid) => sid.replace(/[^A-Za-z0-9._-]+/g, '_').slice(0, 64)
|
|
|
78
78
|
* This session is the only process on its own root, so the single-writer invariant holds naturally; no primary/attached,
|
|
79
79
|
* no failover; version skew (an old primary serving an old action surface) is structurally impossible.
|
|
80
80
|
*/
|
|
81
|
+
/**
|
|
82
|
+
* Toast level for an attention notice. A clean success is informational; anything that needs a
|
|
83
|
+
* closer look (failure, cancellation, unknown exit, deadline, semantic candidate) or a broken
|
|
84
|
+
* relay wake leg stays a warning.
|
|
85
|
+
*/
|
|
86
|
+
export function attentionNotifyLevel(notice) {
|
|
87
|
+
if (notice.transport === 'relay-failed')
|
|
88
|
+
return 'warning';
|
|
89
|
+
return notice.reasonCode === 'task.terminal' && notice.taskState === 'succeeded' ? 'info' : 'warning';
|
|
90
|
+
}
|
|
81
91
|
class SessionBackend {
|
|
82
92
|
rt;
|
|
83
93
|
refreshWidget;
|
|
@@ -100,7 +110,7 @@ class SessionBackend {
|
|
|
100
110
|
const text = `pi-watcher attention [${notice.reasonCode}] watch ${notice.watchId}: ${notice.summary}` + tail;
|
|
101
111
|
if (!notice.ownerSession || notice.ownerSession === this.sessionId) {
|
|
102
112
|
try {
|
|
103
|
-
this.notifyUser(text);
|
|
113
|
+
this.notifyUser(text, attentionNotifyLevel(notice));
|
|
104
114
|
}
|
|
105
115
|
catch { /* display failure does not block */ }
|
|
106
116
|
}
|
|
@@ -120,6 +130,9 @@ class SessionBackend {
|
|
|
120
130
|
async localAck(req, actor) {
|
|
121
131
|
return JSON.stringify(await toServiceResultAsync(req.requestId, () => this.rt.service.ackEpisode(req.requestId, req.episodeId, req.action, req.note, req.until, actor)));
|
|
122
132
|
}
|
|
133
|
+
async localCancelAll(reason, actor) {
|
|
134
|
+
return JSON.stringify(await toServiceResultAsync(undefined, () => this.rt.service.closeAllForOwner(reason, actor)));
|
|
135
|
+
}
|
|
123
136
|
requestWidgetRefresh() {
|
|
124
137
|
this.refreshWidget();
|
|
125
138
|
}
|
|
@@ -226,7 +239,7 @@ export default function watcherExtension(pi, options = {}) {
|
|
|
226
239
|
catch { /* display failure does not block the engine */ }
|
|
227
240
|
}
|
|
228
241
|
});
|
|
229
|
-
const be = new SessionBackend(rt, refreshWidget, text => { ctx.ui?.notify?.(text,
|
|
242
|
+
const be = new SessionBackend(rt, refreshWidget, (text, level) => { ctx.ui?.notify?.(text, level); }, primarySid);
|
|
230
243
|
attentionSink = n => be.handleAttention(n);
|
|
231
244
|
if (sessionClosed) {
|
|
232
245
|
be.close();
|
|
@@ -543,7 +556,7 @@ export default function watcherExtension(pi, options = {}) {
|
|
|
543
556
|
].join('\n');
|
|
544
557
|
};
|
|
545
558
|
pi.registerCommand('watcher', {
|
|
546
|
-
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',
|
|
547
560
|
handler: async (args, ctx) => {
|
|
548
561
|
await ensureBackend(ctx);
|
|
549
562
|
const be = backend;
|
|
@@ -587,6 +600,24 @@ export default function watcherExtension(pi, options = {}) {
|
|
|
587
600
|
ctx.ui?.notify?.(result, parsed.ok === false ? 'error' : 'info');
|
|
588
601
|
return;
|
|
589
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
|
+
}
|
|
590
621
|
const panel = await be.panel(actorOf(ctx));
|
|
591
622
|
const text = JSON.stringify(panel, null, 2);
|
|
592
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
|
+
"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",
|