@maolon/pi-watcher 0.1.1 → 0.1.3

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
@@ -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.3] - 2026-10-08
10
+
11
+ ### Changed
12
+ - 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`.
13
+ - Git flow: `dev` integration branch, release and hotfix PRs into `main` publish to npm.
14
+
15
+ ## [0.1.2] - 2026-10-08
16
+
17
+ ### Changed
18
+ - README: state up front that the optional semantic check uses Jev, with credentials from Pi's `/login`.
19
+
7
20
  ## [0.1.1] - 2026-10-08
8
21
 
9
22
  ### Changed
package/README.md CHANGED
@@ -4,6 +4,11 @@ Watch long-running work from [Pi](https://pi.dev) without babysitting it. pi-wat
4
4
  training jobs, CI pipelines and deadlines from durable evidence. It keeps the session quiet while things progress
5
5
  and wakes the session (through [pi-relay](https://github.com/Maolon/pi-relay)) only when a fact needs a decision.
6
6
 
7
+ For what patterns cannot see (a stalled run, a fix loop that keeps repeating, a log that claims success while
8
+ showing errors), pi-watcher can optionally run a **semantic check with [Jev](https://typesafe.ai)**, TypeSafe's
9
+ fast discriminative classifier, using the Jev credentials you already have in Pi (`/login` → TypeSafe). See
10
+ [Semantic review](#semantic-review-jev-optional).
11
+
7
12
  ```bash
8
13
  pi install npm:@maolon/pi-relay # wake transport (recommended, see "pi-relay" below)
9
14
  pi install npm:@maolon/pi-watcher
@@ -16,7 +21,10 @@ model turn and grows the context just to learn "still running". pi-watcher moves
16
21
 
17
22
  - **Facts first.** Explicit terminal states, exit markers, deadlines and silence are decided by code, not by a model.
18
23
  - **Quiet by default.** Routine progress only updates a status widget. Nothing enters the conversation.
19
- - **Wake on decisions.** Failure, a crossed deadline, a ready dependency or (optionally) a semantic blocker
24
+ - **Semantic check with Jev (optional).** Per watch, Jev answers six bounded questions about a sanitized
25
+ evidence window: progress, blocker, needs a decision, repeating, claim vs evidence, enough context. Its scores
26
+ never override hard facts; they only raise or annotate episodes. Off unless you enable it.
27
+ - **Wake on decisions.** Failure, a crossed deadline, a ready dependency or (optionally) a Jev-detected blocker
20
28
  becomes one episode and one wake. The host then inspects the fresh facts and responds.
21
29
  - **Honest state.** Unknown stays unknown. Delivery, withdrawal and completion are reported only with evidence.
22
30
 
@@ -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 {
@@ -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 */ }
@@ -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;
@@ -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
  }
@@ -226,7 +236,7 @@ export default function watcherExtension(pi, options = {}) {
226
236
  catch { /* display failure does not block the engine */ }
227
237
  }
228
238
  });
229
- const be = new SessionBackend(rt, refreshWidget, text => { ctx.ui?.notify?.(text, 'warning'); }, primarySid);
239
+ const be = new SessionBackend(rt, refreshWidget, (text, level) => { ctx.ui?.notify?.(text, level); }, primarySid);
230
240
  attentionSink = n => be.handleAttention(n);
231
241
  if (sessionClosed) {
232
242
  be.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maolon/pi-watcher",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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",