@osolmaz/pi-workflows 0.7.0 → 0.8.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.
@@ -0,0 +1,119 @@
1
+ # Durable job progress
2
+
3
+ `@osolmaz/pi-workflows/job-progress` lets a remote job publish progress that a Pi Workflows monitor can validate and measure. It uses the existing `pi-workflows.progress.v1` track contract and ETA estimator.
4
+
5
+ ## Report progress
6
+
7
+ Create one reporter for one physical job. Inject the storage write so the package remains independent of a cloud provider.
8
+
9
+ ```ts
10
+ import { createJobProgressReporter } from "@osolmaz/pi-workflows/job-progress";
11
+
12
+ const reporter = createJobProgressReporter({
13
+ application: "example",
14
+ component: "batch-worker",
15
+ jobId: process.env.JOB_ID ?? "local",
16
+ sourceRevision: "abc123",
17
+ contractHash: "def456",
18
+ startedAt: new Date().toISOString(),
19
+ initialTracks: [
20
+ {
21
+ key: "overall",
22
+ data: {
23
+ schema: "pi-workflows.progress.v1",
24
+ status: "running",
25
+ phase: "starting",
26
+ },
27
+ },
28
+ ],
29
+ minimumIntervalMs: 30_000,
30
+ publishTimeoutMs: 15_000,
31
+ publish: async (snapshot, signal) => {
32
+ await bucket.writeText(progressPath, JSON.stringify(snapshot), { signal });
33
+ },
34
+ });
35
+
36
+ await reporter.report({
37
+ phase: "processing",
38
+ tracks: [
39
+ {
40
+ key: "records",
41
+ data: {
42
+ schema: "pi-workflows.progress.v1",
43
+ status: "running",
44
+ phase: "processing",
45
+ completed: 400,
46
+ total: 1_000,
47
+ unit: "records",
48
+ },
49
+ },
50
+ ],
51
+ });
52
+ ```
53
+
54
+ The first update, each phase change, and each terminal update publishes immediately. Other updates are coalesced until `minimumIntervalMs` has passed. Call `flush()` at a durable checkpoint or before exit when the current snapshot has not been published.
55
+
56
+ On process restart, read and validate the existing snapshot and pass it as `previousSnapshot`. The reporter continues its sequence number and rejects an identity, deadline, or terminal-state mismatch.
57
+
58
+ The reporter keeps the latest snapshot after a write failure. The application decides when to log and retry that failure. A timed-out storage write remains serialized until the underlying write settles, so an older write cannot overwrite a newer snapshot. Storage adapters must honor the abort signal and settle after cancellation. A progress failure must not replace receipt or checkpoint validation.
59
+
60
+ ## Finish a job
61
+
62
+ A terminal update gets a finish timestamp and publishes immediately:
63
+
64
+ ```ts
65
+ await reporter.report({
66
+ state: "completed",
67
+ phase: "complete",
68
+ tracks: [
69
+ {
70
+ key: "records",
71
+ data: {
72
+ schema: "pi-workflows.progress.v1",
73
+ status: "completed",
74
+ phase: "complete",
75
+ completed: 1_000,
76
+ total: 1_000,
77
+ unit: "records",
78
+ },
79
+ },
80
+ ],
81
+ });
82
+ ```
83
+
84
+ A terminal snapshot is not a receipt. Receipts, manifests, hashes, and durable application outputs remain authoritative.
85
+
86
+ ## Store and discover snapshots
87
+
88
+ Write one mutable snapshot per physical job:
89
+
90
+ ```text
91
+ <existing-bucket>/<application-prefix>/runs/<job-id>/progress.json
92
+ ```
93
+
94
+ Add these immutable labels to the job or schedule:
95
+
96
+ ```text
97
+ progress_schema=pi-workflows.job-progress.v1
98
+ progress_bucket=<existing-bucket>
99
+ progress_prefix=<application-prefix>/runs
100
+ ```
101
+
102
+ A monitor reads the labels, forms `<progress_prefix>/<job-id>/progress.json`, validates the snapshot with `validateJobProgressSnapshot`, and publishes its tracks with stable keys. It should retain consecutive snapshots so Pi Workflows can estimate a conservative ETA from measured rates.
103
+
104
+ ## Estimate from snapshots
105
+
106
+ ```ts
107
+ import { estimateJobProgress } from "@osolmaz/pi-workflows/job-progress";
108
+
109
+ const result = estimateJobProgress(previousSnapshots);
110
+ for (const estimate of result.estimates) {
111
+ console.log(estimate.key, estimate.remainingMedianMs);
112
+ }
113
+ ```
114
+
115
+ The estimator returns no measured ETA until a track has a known total and enough positive progress samples. A source-provided `sourceEstimatedFinishAt` remains available through the normal progress contract.
116
+
117
+ ## Data boundary
118
+
119
+ Snapshots may contain identifiers, phases, counters, totals, timestamps, and cost totals. Do not put credentials, environment values, input records, model responses, logs, or private content in a snapshot. The strict validator rejects unknown fields and limits the encoded snapshot to 64 KiB.
package/docs/MONITOR.md CHANGED
@@ -242,6 +242,11 @@ One monitor can track several processes. Each uses a stable progress key. A miss
242
242
 
243
243
  The progress estimator treats each key independently. A phase, unit, total, or counter reset in one track does not reset another track.
244
244
 
245
+ Remote Jobs can publish the same tracks in a strict
246
+ [`pi-workflows.job-progress.v1` snapshot](JOB_PROGRESS.md). A monitor discovers
247
+ the snapshot from immutable Job labels, validates its identity, and submits its
248
+ tracks without copying log-derived guesses into progress fields.
249
+
245
250
  ## Interval and lifetime
246
251
 
247
252
  The normal workflow engine remains finite. The built-in monitor therefore retains the 1,000-check safety ceiling and must not claim to be mathematically unbounded.
@@ -99,6 +99,6 @@ git diff --check
99
99
  Run in Tools after deletion:
100
100
 
101
101
  ```bash
102
- python3 agents/sync-skills.py monitor
102
+ python3 agents/sync-skills.py
103
103
  npx -y @simpledoc/simpledoc check
104
104
  ```
@@ -0,0 +1,176 @@
1
+ # Durable job progress
2
+
3
+ ## Problem
4
+
5
+ Long-running remote jobs can expose state only through logs and final receipts. A monitor can confirm that a job is running, but it cannot give a reliable progress value or remaining time when the job does not publish a completed count, a total, and source timestamps.
6
+
7
+ xTap Pool and OurModels need the same durable progress contract. The contract must work with their existing Hugging Face Buckets, survive worker restarts, and use the ETA estimator that Pi Workflows already uses for `pi-workflows.progress.v1` tracks.
8
+
9
+ ## Requirements
10
+
11
+ The implementation must:
12
+
13
+ - add one storage-neutral job progress API to `@osolmaz/pi-workflows`
14
+ - reuse `pi-workflows.progress.v1` for progress tracks
15
+ - write one mutable snapshot for each physical job in the application's existing Bucket
16
+ - let monitors discover the snapshot from immutable job labels
17
+ - let Pi Workflows validate the snapshot and estimate remaining time from repeated samples
18
+ - let xTap Pool report restore, review, recovery, and publication progress
19
+ - let OurModels report discovery, processing, cache, and publication progress
20
+ - keep receipts, content hashes, manifests, and databases authoritative
21
+ - avoid secrets, post text, model output, and other private content in progress snapshots
22
+ - preserve one physical enrichment job at a time during the xTap Pool change
23
+ - leave the OurModels replacement schedule suspended until a paid run is separately approved
24
+
25
+ ## Non-goals
26
+
27
+ This work does not add:
28
+
29
+ - a new remote store
30
+ - a metrics database
31
+ - a second ETA protocol
32
+ - a Pi core change
33
+ - a service or daemon
34
+ - a compatibility path for an older snapshot schema
35
+ - automatic authority to retry, deploy, publish, or spend money
36
+
37
+ ## Public contract
38
+
39
+ The npm package exports a new subpath:
40
+
41
+ ```ts
42
+ import {
43
+ createJobProgressReporter,
44
+ estimateJobProgress,
45
+ validateJobProgressSnapshot,
46
+ type JobProgressSnapshot,
47
+ } from "@osolmaz/pi-workflows/job-progress";
48
+ ```
49
+
50
+ A snapshot has schema `pi-workflows.job-progress.v1` and contains:
51
+
52
+ - stable application and component names
53
+ - the physical job identifier
54
+ - source and work-contract identifiers
55
+ - a monotonic sequence number
56
+ - job state and current phase
57
+ - start, update, optional deadline, and optional finish timestamps
58
+ - one or more keyed `pi-workflows.progress.v1` tracks
59
+ - optional settled cost and active reservation facts
60
+
61
+ The validator is strict. It rejects unknown fields, duplicate track keys, invalid timestamps, non-finite values, invalid progress tracks, and oversized snapshots.
62
+
63
+ The reporter accepts an injected asynchronous `publish(snapshot)` callback. Pi Workflows does not import a Hugging Face client. The reporter:
64
+
65
+ - keeps sequence numbers monotonic within the process
66
+ - rejects regressions within one phase and epoch
67
+ - coalesces frequent updates with a configurable minimum interval
68
+ - flushes phase changes and terminal states immediately
69
+ - bounds publication time with an abort deadline
70
+ - preserves the most recent unsent snapshot after a transient publication failure
71
+ - never includes arbitrary metadata or environment values
72
+
73
+ A terminal snapshot is operational evidence only. The application's receipt and durable output validation still decide whether work succeeded.
74
+
75
+ ## Storage and discovery
76
+
77
+ Each application writes snapshots to its existing Bucket.
78
+
79
+ xTap Pool uses:
80
+
81
+ ```text
82
+ osolmaz/xtap-pool-bucket/operations/enrichment/runs/<job-id>/progress.json
83
+ ```
84
+
85
+ OurModels uses:
86
+
87
+ ```text
88
+ osolmaz/ourmodels-data/<prefix>/operations/community-posts/runs/<job-id>/progress.json
89
+ ```
90
+
91
+ Each schedule supplies these immutable labels:
92
+
93
+ ```text
94
+ progress_schema=pi-workflows.job-progress.v1
95
+ progress_bucket=<bucket>
96
+ progress_prefix=<path-before-job-id>
97
+ ```
98
+
99
+ A monitor reads the labels, appends the physical job identifier and `progress.json`, reads the snapshot with existing local Hugging Face authentication, validates it, and publishes each track under a stable workflow progress key. The monitor does not trust an ETA string from logs. It uses source finish time when the snapshot provides one, or the existing conservative estimator after enough measured samples.
100
+
101
+ ## xTap Pool tracks
102
+
103
+ The enrichment worker reports these stable tracks when facts are measurable:
104
+
105
+ | Key | Unit | Source |
106
+ | ------------------ | ---------- | -------------------------------------------------- |
107
+ | `database-restore` | bytes | downloaded database bytes and expected object size |
108
+ | `registry-replay` | events | replayed registry events and discovered total |
109
+ | `registry-scan` | candidates | durable scan cursor and fixed candidate total |
110
+ | `queue` | records | terminal records and durable queue total |
111
+ | `publication` | bytes | uploaded and verified database bytes |
112
+ | `overall` | phases | completed phases and fixed phase count |
113
+
114
+ Phase-only states remain valid when a total is not yet known. The worker must not invent totals.
115
+
116
+ The existing three unresolved records must receive durable outcomes under the fixed full-response deadline or become exactly validated blocked records. The worker then publishes and verifies the index before its final receipt is accepted.
117
+
118
+ ## OurModels tracks
119
+
120
+ The community-posts worker reports these stable tracks when facts are measurable:
121
+
122
+ | Key | Unit | Source |
123
+ | ----------------- | ------- | ----------------------------------------------- |
124
+ | `model-discovery` | models | discovered and inspected model count |
125
+ | `community-posts` | models | processed models and fixed discovered total |
126
+ | `cache` | records | durable cached model results and expected total |
127
+ | `publication` | bytes | uploaded and verified artifact bytes |
128
+ | `overall` | phases | completed phases and fixed phase count |
129
+
130
+ The worker continues to use its existing receipt and manifest rules. A progress write failure must not corrupt a useful checkpoint or published result.
131
+
132
+ ## Delivery sequence
133
+
134
+ 1. Add and release `@osolmaz/pi-workflows/job-progress` as version `0.8.0`.
135
+ 2. Add snapshot discovery guidance to the bundled monitor skill.
136
+ 3. Update xTap Pool to use `0.8.0`, add measured callbacks, and add schedule labels.
137
+ 4. Merge and deploy xTap Pool, then replace the old suspended schedule.
138
+ 5. End the old physical job only when the replacement source is ready and the one-job rule can be preserved.
139
+ 6. Start one instrumented xTap Pool recovery job under the existing restoration budget.
140
+ 7. Update OurModels to use `0.8.0`, add measured callbacks, and replace old schedules with one suspended instrumented schedule.
141
+ 8. Do not start a paid OurModels run until its measured cost range and ceiling receive the required approval.
142
+ 9. Start a Pi monitor that reads both progress surfaces and displays current progress and ETA.
143
+
144
+ ## Acceptance checks
145
+
146
+ Pi Workflows must pass:
147
+
148
+ ```bash
149
+ npm run check
150
+ npm run test:e2e
151
+ npx slophammer-ts@latest dry .
152
+ npx slophammer-ts@latest check . --only ts.dependency-boundaries-required
153
+ ```
154
+
155
+ Tests must cover strict validation, unknown fields, duplicate keys, monotonic updates, phase resets, coalescing, transient publication failure, deadline abort, terminal flush, and ETA estimation from snapshots.
156
+
157
+ xTap Pool must pass its repository checks, including mutation testing. A live recovery must show a valid snapshot in the existing index Bucket, and repeated monitor samples must produce a measured ETA when a track has a known total and positive progress.
158
+
159
+ OurModels must pass:
160
+
161
+ ```bash
162
+ npm run check
163
+ npm run coverage
164
+ npm run dry
165
+ npm run mutate
166
+ node scripts/test-bounds-engine.mjs
167
+ node scripts/validate-model-data.mjs
168
+ ```
169
+
170
+ Its replacement schedule must remain suspended with only the approved two secrets until a paid run is authorized.
171
+
172
+ ## Recovery
173
+
174
+ If a progress publication fails, the worker keeps useful work and retries only the latest snapshot at the next bounded update. If the job ends before that succeeds, the receipt and durable outputs remain authoritative and the monitor marks progress stale.
175
+
176
+ If a repeated deterministic worker defect appears, stop the affected job and schedule. Preserve all existing Bucket objects and return the exact failing phase, source revision, snapshot, receipt state, and last valid durable outputs.
package/docs/workflows.md CHANGED
@@ -369,6 +369,16 @@ and resume later by running that wait again from the beginning.
369
369
  A monitor uses the session's single active workflow slot. It does not provide
370
370
  cron syntax, calendar scheduling, OS notifications, or a background service.
371
371
 
372
+ ### Durable remote Job progress
373
+
374
+ Remote workers can publish the same progress tracks through
375
+ `@osolmaz/pi-workflows/job-progress`. The storage-neutral reporter writes a
376
+ strict, versioned snapshot through an application-supplied callback. A monitor
377
+ can read consecutive snapshots from an existing remote store and use the normal
378
+ progress estimator without treating logs as durable state. See
379
+ [JOB_PROGRESS.md](JOB_PROGRESS.md) for the API, snapshot schema, restart rules,
380
+ and discovery labels.
381
+
372
382
  ## The step contract
373
383
 
374
384
  Every `agent` prompt ends with a step contract block naming the workflow, the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osolmaz/pi-workflows",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Workflow and controller runtime with a live terminal viewer for the pi coding agent",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -39,6 +39,10 @@
39
39
  "./controllers": {
40
40
  "types": "./dist/controllers/index.d.ts",
41
41
  "default": "./dist/controllers/index.js"
42
+ },
43
+ "./job-progress": {
44
+ "types": "./dist/job-progress/index.d.ts",
45
+ "default": "./dist/job-progress/index.js"
42
46
  }
43
47
  },
44
48
  "publishConfig": {
@@ -95,6 +95,18 @@ Submit observed facts. The workflow computes rates, confidence, remaining work,
95
95
 
96
96
  For several concurrent processes, publish one stable track per process. The Pi widget and viewers show them separately and keep each ETA independent.
97
97
 
98
+ ### Read durable job snapshots
99
+
100
+ A remote job can advertise a `pi-workflows.job-progress.v1` snapshot with these immutable labels:
101
+
102
+ - `progress_schema=pi-workflows.job-progress.v1`
103
+ - `progress_bucket=<existing-bucket>`
104
+ - `progress_prefix=<path-before-job-id>`
105
+
106
+ When these labels exist, form `<progress_prefix>/<physical-job-id>/progress.json`, read it from the named existing store with an already authorized credential, and validate it with the installed `@osolmaz/pi-workflows/job-progress` API. Reject a snapshot whose job ID, source revision, or work-contract identity does not match the observed Job. Do not follow a path or credential named inside unvalidated data.
107
+
108
+ Publish the snapshot tracks under stable keys. Preserve consecutive samples so the workflow can estimate ETA from measured progress. Treat a stale or missing snapshot as unavailable progress, not as proof that the Job stopped. Receipts, hashes, manifests, and final outputs remain the completion evidence.
109
+
98
110
  ## Apply finish rules
99
111
 
100
112
  ### Still active
@@ -0,0 +1,24 @@
1
+ export {
2
+ createJobProgressReporter,
3
+ estimateJobProgress,
4
+ type JobProgressReporter,
5
+ } from "./reporter.js";
6
+ export {
7
+ isTerminalJobProgressState,
8
+ MAX_JOB_PROGRESS_BYTES,
9
+ MAX_JOB_PROGRESS_TRACKS,
10
+ validateJobProgressSnapshot,
11
+ } from "./validation.js";
12
+ export {
13
+ JOB_PROGRESS_SCHEMA,
14
+ type JobProgressCost,
15
+ type JobProgressEstimate,
16
+ type JobProgressIdentity,
17
+ type JobProgressPublish,
18
+ type JobProgressPublishResult,
19
+ type JobProgressReporterOptions,
20
+ type JobProgressSnapshot,
21
+ type JobProgressState,
22
+ type JobProgressTrack,
23
+ type JobProgressUpdate,
24
+ } from "./types.js";