instar 1.3.1600 → 1.3.1602
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/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +8 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +65 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/core/AppConversationService.d.ts +39 -1
- package/dist/core/AppConversationService.d.ts.map +1 -1
- package/dist/core/AppConversationService.js +137 -2
- package/dist/core/AppConversationService.js.map +1 -1
- package/dist/core/AppConversationStore.d.ts +2 -0
- package/dist/core/AppConversationStore.d.ts.map +1 -1
- package/dist/core/AppConversationStore.js +4 -0
- package/dist/core/AppConversationStore.js.map +1 -1
- package/dist/core/AppMediaReplicator.d.ts +241 -0
- package/dist/core/AppMediaReplicator.d.ts.map +1 -0
- package/dist/core/AppMediaReplicator.js +903 -0
- package/dist/core/AppMediaReplicator.js.map +1 -0
- package/dist/core/AppMediaStore.d.ts +28 -0
- package/dist/core/AppMediaStore.d.ts.map +1 -1
- package/dist/core/AppMediaStore.js +87 -13
- package/dist/core/AppMediaStore.js.map +1 -1
- package/dist/core/BrowserCareProbes.d.ts +15 -0
- package/dist/core/BrowserCareProbes.d.ts.map +1 -0
- package/dist/core/BrowserCareProbes.js +78 -0
- package/dist/core/BrowserCareProbes.js.map +1 -0
- package/dist/core/MachineLockStateReader.d.ts +34 -0
- package/dist/core/MachineLockStateReader.d.ts.map +1 -0
- package/dist/core/MachineLockStateReader.js +114 -0
- package/dist/core/MachineLockStateReader.js.map +1 -0
- package/dist/core/MeshRpc.d.ts +1 -1
- package/dist/core/MeshRpc.d.ts.map +1 -1
- package/dist/core/MeshRpc.js +4 -0
- package/dist/core/MeshRpc.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +14 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +60 -2
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/WindowRunLivenessAuthority.d.ts.map +1 -1
- package/dist/core/WindowRunLivenessAuthority.js +11 -1
- package/dist/core/WindowRunLivenessAuthority.js.map +1 -1
- package/dist/core/appConversationText.d.ts +5 -0
- package/dist/core/appConversationText.d.ts.map +1 -1
- package/dist/core/appConversationText.js +17 -2
- package/dist/core/appConversationText.js.map +1 -1
- package/dist/core/appInboundDispatch.d.ts +14 -0
- package/dist/core/appInboundDispatch.d.ts.map +1 -1
- package/dist/core/appInboundDispatch.js +25 -0
- package/dist/core/appInboundDispatch.js.map +1 -1
- package/dist/core/types.d.ts +10 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/data/standards-guard-index.json +1 -1
- package/dist/data/standards-guard-index.meta.json +2 -2
- package/dist/data/standards-registry.meta.json +1 -1
- package/dist/monitoring/machineMaintenance/BrowserCare.d.ts +166 -0
- package/dist/monitoring/machineMaintenance/BrowserCare.d.ts.map +1 -0
- package/dist/monitoring/machineMaintenance/BrowserCare.js +334 -0
- package/dist/monitoring/machineMaintenance/BrowserCare.js.map +1 -0
- package/dist/monitoring/machineMaintenance/lockState.d.ts +34 -0
- package/dist/monitoring/machineMaintenance/lockState.d.ts.map +1 -0
- package/dist/monitoring/machineMaintenance/lockState.js +53 -0
- package/dist/monitoring/machineMaintenance/lockState.js.map +1 -0
- package/dist/monitoring/machineMaintenance/readings.d.ts +4 -0
- package/dist/monitoring/machineMaintenance/readings.d.ts.map +1 -1
- package/dist/monitoring/machineMaintenance/readings.js +2 -1
- package/dist/monitoring/machineMaintenance/readings.js.map +1 -1
- package/dist/native/instar-desktop-host +0 -0
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +2 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +8 -1
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.js +3 -3
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/mobileConversationRoutes.d.ts +48 -0
- package/dist/server/mobileConversationRoutes.d.ts.map +1 -1
- package/dist/server/mobileConversationRoutes.js +259 -33
- package/dist/server/mobileConversationRoutes.js.map +1 -1
- package/dist/server/mobileMediaRoutes.d.ts.map +1 -1
- package/dist/server/mobileMediaRoutes.js +11 -1
- package/dist/server/mobileMediaRoutes.js.map +1 -1
- package/dist/server/mobilePairingRoutes.d.ts.map +1 -1
- package/dist/server/mobilePairingRoutes.js +34 -12
- package/dist/server/mobilePairingRoutes.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +47 -3
- package/dist/server/routes.js.map +1 -1
- package/dist/testing/selfActionRegistry.d.ts.map +1 -1
- package/dist/testing/selfActionRegistry.js +41 -0
- package/dist/testing/selfActionRegistry.js.map +1 -1
- package/package.json +1 -1
- package/scripts/blocking-decision-surfaces-baseline.json +22 -2
- package/src/data/builtin-manifest.json +64 -64
- package/src/data/standards-guard-index.json +1 -1
- package/src/data/standards-guard-index.meta.json +2 -2
- package/src/data/standards-registry.meta.json +1 -1
- package/src/data/state-coherence-registry.json +37 -0
- package/src/scaffold/templates.ts +2 -1
- package/src/templates/scripts/browser/chrome-close.sh +97 -0
- package/src/templates/scripts/browser/chrome-js.sh +2 -0
- package/src/templates/scripts/browser/chrome-launch.sh +16 -3
- package/src/templates/scripts/browser/chrome-ledger.sh +49 -0
- package/src/templates/scripts/browser/chrome-renew.sh +38 -0
- package/src/templates/scripts/browser/chrome-unsaved.sh +83 -0
- package/upgrades/1.3.1601.md +68 -0
- package/upgrades/1.3.1602.md +24 -0
- package/upgrades/side-effects/machine-self-maintenance-browser-care-lock-reading.md +98 -0
- package/upgrades/side-effects/mobile-native-chat-media-cross-machine.md +131 -0
- package/upgrades/side-effects/native-conversations-gaps.md +129 -0
- package/upgrades/side-effects/window-run-liveness-later-loss.md +81 -0
- package/upgrades/1.3.1600.md +0 -34
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Side-Effects Review — media in app conversations, increment 1 (copies between machines)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `mobile-native-chat-media-cross-machine`
|
|
4
|
+
**Date:** `2026-11-02`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `independent review 2026-11-02: CONCERN, six findings, each fixed below (confirming pass not run; brief: --second-pass false)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Builds the rest of increment 1 of `docs/specs/mobile-native-chat-media.md` (converged, approved), the cross-machine half that PR #2573 left out (its side-effects file, `mobile-native-chat-media-inc1.md`, listed it). New: `AppMediaReplicator` (`src/core/AppMediaReplicator.ts`) with the serve side of a new mesh verb `app-media-pull` (≤ 1 MiB slices of a blob an unpurged local event references, async positional reads through an `O_NOFOLLOW` + inode-checked descriptor, at most 2 served at once; the held-hash inventory, paged or as a diff against a version the asker holds; a nudge), the eager background pull (5 s tick, ≤ 8 slices per tick, disk-floor pause, bounded load pause, per-blob backoff 30 s → 1 h and a 7-day give-up, per-peer breaker 5 failures → 15 min, 3 opens/day → closed until tomorrow), hourly peer inventories, `singleCopyBlobs` and the §4.12 alarm ladder (re-queue on peers by nudge after an hour, then one notice; offline peer > 24 h; unmeasurable > 2 h), `unrecoverableBlobs` with its notice. `AppMediaStore` gains `pulls/`, three tables (`pulls`, `pull_peers`, `pending_deliveries`), `publishPulled` and `heldHashes`. `AppConversationService` builds the replicator, marks it dirty on replicated media/purge events, and implements the owner-side delivery (`sessionLinesFor`: pull for ≤ 60 s, else `[file still copying: <name>]` + a durable row; `resolvePending`: one `[file now available: <path>] (for the message "…")` line into the live session when the blob lands, de-duplicated by attachment; otherwise the line rides the next delivery's context and is removed once delivered; rows older than 7 days become a "could not be copied" line). The download route answers `202 {state: "fetching", retryAfterMs}` and starts a priority pull; `app-media-path` pulls for ≤ 30 s and says why when it cannot; `/health?scope=pool` adds `appMediaPool` through the existing pool read helper. `MeshRpc` gains the verb in the registered-peer read class; `server.ts` registers the handler and attaches the transport (the same `MeshRpcClient` + `peerUrl` every other verb uses, peers from the identity registry, online from the pool registry, load from the work-admission verdict). CLAUDE.md section line refreshed in place for existing agents; CapabilityIndex text; a self-action controller for the copier's notices and nudges. Tier 2.
|
|
11
|
+
|
|
12
|
+
**What remains of the spec after this PR:** increments 2–5 (server-side transcription, Telegram/Slack continuation, push preview, the iOS app). See "Declared limits" for the one part of §4.12 whose trigger does not exist yet.
|
|
13
|
+
|
|
14
|
+
## Decision-point inventory
|
|
15
|
+
|
|
16
|
+
- Serve a slice — add — invariant: hash format, offset range, an unpurged local reference, a held blob opened `O_NOFOLLOW` with a matching inode, ≤ 2 concurrent serves.
|
|
17
|
+
- Keep a pulled copy — add — invariant: slice offset/size equal the request and the reference; whole-file length and SHA-256 equal the reference; nothing else is ever kept.
|
|
18
|
+
- Pause pulls — add — invariant: the existing disk floor (5% / 2 GiB); the existing work-admission load verdict on its two worst values, at most an hour at a time.
|
|
19
|
+
- Give up on a blob / open a peer breaker — add — invariant: fixed backoff, 7-day give-up, 5-failure breaker, 3-opens-per-day latch.
|
|
20
|
+
- Single-copy alarm — add — invariant: counts from inventories younger than 2 h; null (never 0) when unmeasurable; fixed 1 h / 24 h / 2 h thresholds.
|
|
21
|
+
- Dispatch with "still copying" — add — invariant: fixed 60 s deadline; follow-up de-duplicated by attachment id.
|
|
22
|
+
- Download `202 fetching` vs `404 media-unavailable` — modify — `404` only for an unrecoverable blob or a machine with no mesh transport; otherwise `202`.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 1. Over-block
|
|
27
|
+
|
|
28
|
+
- **A blob only one machine holds while that machine is offline** cannot be downloaded elsewhere: the app sees `202` until it gives up polling (60 s, its own rule), and after 7 days `404`. By design — the bytes are not reachable.
|
|
29
|
+
- **Busy serve side:** a third concurrent slice request is answered `busy`; the asker retries next tick with no backoff penalty.
|
|
30
|
+
- **Load pause:** background copying stops for up to an hour on a machine reporting `saturated`/`impaired`; on-demand pulls (download, app-media-path, dispatch) are not paused by load. Only the disk floor stops them.
|
|
31
|
+
- **A peer whose copies fail verification** is skipped even for files only it holds (15 minutes, or the rest of the day after three opens). Files it holds stay on it; the single-copy and unrecoverable counts make this visible.
|
|
32
|
+
|
|
33
|
+
## 2. Under-block
|
|
34
|
+
|
|
35
|
+
- **Admission cap is still soft across machines** (unchanged, spec §4.7). Pulls are never stopped by the cap, only by the disk floor.
|
|
36
|
+
- **A machine lost before its blobs were copied loses them** (spec §4.12 residual risk). Eager pull and the single-copy alarm shorten and expose the window; nothing makes it zero.
|
|
37
|
+
- **Inventories are in memory.** After a restart `singleCopyBlobs` reads null until the next inventory round (at most an hour) — honest, not a fabricated 0 — and the alarm episode clocks restart.
|
|
38
|
+
|
|
39
|
+
### Declared limits (no code change)
|
|
40
|
+
|
|
41
|
+
- **"The owner pulls before dispatch" has no cross-machine trigger yet.** The parent spec (mobile-native-conversations.md §4.8) forwards a send accepted on a non-owner to the owner; that forward is not built (a non-owner still answers `503 owner-unavailable` after persisting — `AppConversationService` header). So today the dispatching machine is always the accepting machine, which already holds the blobs it just published. The pull-before-dispatch code (`sessionLinesFor`) is in place on the dispatch path and is exercised by the integration test directly on a machine that lacks the blob; it activates for real when the forward ships. It also covers the case where the dispatching machine's own blob went missing.
|
|
42
|
+
- **The follow-up line is typed into a live session only.** It never spawns a session; with no live session it waits for the next delivery's context. A conversation that never receives another message never sees it (the file is still listed in history and opens with app-media-path).
|
|
43
|
+
- **Nudges are best effort.** A nudge whose send fails is reported as a degradation, not retried in that episode; the notice an hour later is the backstop.
|
|
44
|
+
|
|
45
|
+
## 3. Level-of-abstraction fit
|
|
46
|
+
|
|
47
|
+
Byte copying sits in one class beside the media store and below the routes; it rides the existing signed MeshRpc transport (`MeshRpcClient`, the `/mesh/rpc` dispatcher, `peerUrl`) rather than a new channel, and its own verb rather than `working-set-pull` (whose serve side reads and hashes whole files synchronously, spec §3). Hashing runs in the existing media worker. The load signal is the existing work-admission verdict; the pool health read uses the existing `readReaperPoolPeers` helper with its rate limiter, roster accounting and bounded body. The pending "still copying" rows live in the media index beside upload state, so they are recovered with it.
|
|
48
|
+
|
|
49
|
+
## 4. Signal vs authority compliance
|
|
50
|
+
|
|
51
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
52
|
+
|
|
53
|
+
- [x] No judgment-based block/allow surface: every refusal or pause is a fixed invariant (hash equality, sizes, offsets, references, counts, timers, the existing disk floor and load verdict). The verb adds reach, never authority: a peer can only cause this machine to fetch bytes an event it already holds refers to, and only bytes that hash to that reference are kept.
|
|
54
|
+
|
|
55
|
+
## 4b. Judgment-point check (Judgment Within Floors standard)
|
|
56
|
+
|
|
57
|
+
No new static heuristic at a competing-signals decision point; every decision above is an invariant declared in the spec's `## Decision points touched` table (pull give-up, breaker, disk-floor and load pauses, the 60 s dispatch deadline).
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 5. Interactions
|
|
62
|
+
|
|
63
|
+
- **Purge and orphans:** a purged attachment is not `allRefs`, so it is never served, wanted or counted; a pull row and partial file for a hash that stops being wanted are removed on the next wanted refresh. The orphan sweep now also skips a hash named by a pending-delivery row. Pulled blobs referenced only by deleted conversations land in `deleted/`.
|
|
64
|
+
- **Quarantine:** `publishPulled` uses the same `publish` path, so a verified pulled copy also retires a quarantined copy of that hash (renamed aside, never deleted), as a fresh upload does.
|
|
65
|
+
- **Disk floor:** a part already complete when the floor is hit is kept (verified, not published) and published on a later tick without another request.
|
|
66
|
+
- **Dispatch timing:** the send route now awaits `sessionLinesFor`; when the blob is local (always, today) there is no wait. When a pull is needed the phone's request can take up to 60 s before it answers.
|
|
67
|
+
- **Concurrency:** the pull tick is single-flight; `want()` triggers a tick, which returns immediately if one is running. `resolvePending` claims a row (`waiting → ready`) before typing the line, so two landings never type twice.
|
|
68
|
+
|
|
69
|
+
## 6. External surfaces
|
|
70
|
+
|
|
71
|
+
- **Other machines of the agent:** a new mesh verb. An older peer answers `no-handler` (501); the puller treats it as a failed attempt for that blob (backoff), never a verification failure. An older peer asking this machine gets a normal answer.
|
|
72
|
+
- **Phones:** `GET /mobile/media/:sha256` can now answer `202 {state: "fetching", retryAfterMs: 2000}`; `404 media-unavailable` now means unrecoverable or no mesh.
|
|
73
|
+
- **Sessions:** new lines `[file still copying: <name>]`, `[app:…] [file now available: <path>] (for the message "…")`, and a "Files from earlier messages" block in the context file. app-media-path's failure line now names why.
|
|
74
|
+
- **Operator:** up to three new attention items per machine (`app-media:single-copy:<machine>`, `app-media:unrecoverable:<machine>`, `app-media:peer-breaker:<peer>`), each raised once per episode and cleared when the condition ends.
|
|
75
|
+
- **Persistent state:** `<stateDir>/app-media/pulls/`, three tables in `app-media/uploads/index.sqlite`.
|
|
76
|
+
|
|
77
|
+
## 6b. Operator-surface quality
|
|
78
|
+
|
|
79
|
+
No operator surface added (no dashboard or approval page) — not applicable.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
84
|
+
|
|
85
|
+
- Blobs: **unified by full replication** — this PR is that replication (every machine pulls every unpurged blob an event it holds references, verified).
|
|
86
|
+
- `singleCopyBlobs`: **measured from peers' inventories** (pulled hourly over the mesh); null when any registered peer's inventory is missing or older than 2 hours.
|
|
87
|
+
- Pull state, breakers, pending deliveries: **machine-local by design** — each describes what this machine holds and is copying; the pending rows belong to the machine whose session was told "still copying".
|
|
88
|
+
- `/health` `appMedia`: **proxied on read** with `?scope=pool` (`appMediaPool`), unreachable machines in `failed`.
|
|
89
|
+
- Notices: one per condition per machine; the condition keys carry the machine id, so two machines never raise the same item for each other.
|
|
90
|
+
|
|
91
|
+
## 8. Rollback cost
|
|
92
|
+
|
|
93
|
+
Revert the PR: the verb disappears (peers on this version see `no-handler` and back off), copying stops, files stay where they are; `pulls/` and the three tables are left unused. Or turn `mobilePairing.native.enabled` off (the media flag gates phone routes only; copying of already stored files continues while native conversations are on, so switching media off never strands a copy). Dark on the fleet.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Conclusion
|
|
98
|
+
|
|
99
|
+
The remaining increment-1 surface ships with every pull verified before storing and every loop braked (per-tick slice cap, backoff, give-up, breaker, latch, single-flight, serve concurrency cap). Found while building: a part file already complete (left by the disk floor) would have triggered a zero-length slice request and been counted as a verification failure; it is now verified and published without a request. The pull-before-dispatch path has no cross-machine trigger until the parent spec's owner forward ships (declared above). A second-pass review is normally required for dispatch changes; the build brief for this PR set `--second-pass false`, so none was run, and the reviewer of the PR should treat this as the first independent read.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Fix after the first Local Verify
|
|
104
|
+
|
|
105
|
+
The full Local Verify failed 9 integration tests in `tests/integration/health-stale-job-failures.test.ts` and `tests/integration/lease-medium-health.test.ts`. Cause: the `appMediaPool` branch read `req.query.scope` unguarded, and those tests (like any caller that drives `router.handle` without Express's query parser) send a request with no `query`, so every `/health` call threw `Cannot read properties of undefined (reading 'scope')`. Fixed with `req.query?.scope`; both files pass again, with the media tests and `health-auth-gating`. No test was changed.
|
|
106
|
+
|
|
107
|
+
## Fixes after the independent review (CONCERN)
|
|
108
|
+
|
|
109
|
+
1. **Purge during the hash** (medium, confirmed): `finishPart` awaited the worker hash and then published without re-checking the reference, so a purge landing mid-hash let purged bytes reach `blobs/` until the next sweep. Now the reference is re-checked after the hash (the partial copy, pull row and waiters are dropped), and `AppMediaStore.publishPulled` itself discards a file whose hash has no unpurged reference. Test: a purge applied while the last slice is in flight.
|
|
110
|
+
2. **`202 fetching` only when a copy can start** (confirmed): the route now asks `canFetch` and answers `404 media-unavailable` with the reason when there is no mesh, the blob was given up on, the machine is below the disk floor, or no online peer with a closed breaker could hold it (a fresh inventory lacking the hash rules a peer out). `whyNotHere` names the disk floor and the background load pause. **Load is not a refusal for an on-demand copy**: spec §4.12 pauses the eager background pulls under load, and the download's pull is the on-demand latency path, which is not paused, so a `202` under load is true. Tests: unit `canFetch`, integration 404 with the reason.
|
|
111
|
+
3. **Backoff reset on every poll or nudge** (confirmed): `want()` and the nudge set `nextAttemptAt = 0` each time, so an app polling every 2 s retried every 2 s. Now an ask or nudge skips a file's backoff at most once per backoff window (`hurry`); a nudge still re-arms a given-up file once (the peer names itself a holder, §4.12). Test: ten asks + ten nudges in a window → one extra attempt.
|
|
112
|
+
4. **Timed-out waiters stayed in the list** (confirmed): removed on timeout. Test.
|
|
113
|
+
5. **Blame for a failed hash** (confirmed): chose the simpler rule, *one peer per partial copy*. The peer that served offset 0 serves every later slice; if it stops serving (offline, breaker, refusal, no answer) the partial copy is discarded and restarts from another peer. A failed whole-file hash is then blamed on exactly that peer. The pin is in memory: after a restart a partial file with no known source restarts from 0 (cost: re-copying at most one partial file per blob). The serving side re-hashes a blob whose size or mtime changed since its last check before serving it, and quarantines a damaged one locally (`verifyBeforeServe`), so a damaged copy is not sent. Tests: restart-from-another-peer with blame on the bad peer only; serve-side quarantine.
|
|
114
|
+
6. **Gate wording** (confirmed): the spec (Configuration) requires pulls to keep running with the media flag off, so the handler, tick and inventory stay on `mobilePairing.native.enabled`; the upgrade note was corrected to say so.
|
|
115
|
+
|
|
116
|
+
## Second-pass review (if required)
|
|
117
|
+
|
|
118
|
+
Not run (brief: `--second-pass false`).
|
|
119
|
+
|
|
120
|
+
## Evidence pointers
|
|
121
|
+
|
|
122
|
+
- `tests/unit/app-media-replicator.test.ts` (21 tests), `tests/unit/PostUpdateMigrator-appMedia.test.ts` (copy-line refresh), `src/testing/selfActionRegistry.ts` controller `app-media-replication`
|
|
123
|
+
- `tests/integration/app-media-cross-machine.test.ts` (9 tests), `tests/e2e/app-media-cross-machine-alive.test.ts`
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Class-Closure Declaration (display-only mirror)
|
|
128
|
+
|
|
129
|
+
- **`defectClass`** — `unbounded-self-action` (the copier nudges peers and raises notices on its own; it retries pulls on its own).
|
|
130
|
+
- **`closure`** — `guard`.
|
|
131
|
+
- **`guardEvidence`** — `{enforcementType: ratchet, citation: tests/unit/self-action-convergence.test.ts (controller app-media-replication, src/testing/selfActionRegistry.ts), howCaught: under a file held alone forever with every peer online, the copier nudges once per episode and raises the single-copy notice once (the raised latch keyed by condition and reason), so the action count is 2 at any horizon}`. Per-blob retries are bounded by the backoff and the 7-day give-up (`tests/unit/app-media-replicator.test.ts`, "backs off 30 s → 1 h, gives up after 7 days").
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Side-Effects Review — native app conversations: lossless list, pool reads, Telegram token removal, approval method
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `native-conversations-gaps`
|
|
4
|
+
**Date:** `2026-11-02`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not run (declared --second-pass false by the brief)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Closes four gaps an app builder found running the iPhone app against a real server with no Telegram, each measured against the converged spec `docs/specs/mobile-native-conversations.md`:
|
|
11
|
+
|
|
12
|
+
1. `GET /mobile/conversations` used a cursor that tracked only app conversations while the first page also carried up to 200 Telegram topics and was then cut to 200, so app conversations could fall off page one and never come back. The list now has one total order (newest activity, ties by `app:<uuid>` / `telegram:<topicId>`, plain string compare — the same order the store's SQLite keyset uses) and one cursor `<activity>|<sortKey>` (the store's existing cursor shape, so old cursors still parse). Every page merges the store's page with the Telegram topics strictly after the cursor (`mergeConversationPages`, `src/server/mobileConversationRoutes.ts`).
|
|
13
|
+
2. `?scope=pool` on the list and on `GET /mobile/chat/app/:key/messages` (spec §4.5, §4.8) now fans out to every peer's plain read with the agent's Bearer token (`poolPeerFetch`, `isPeerUrlAllowedForCredentials`, as `/telegram/topics/:id/messages?scope=pool` does), merges losslessly on the same cursor (list) or `(at, eventId)` boundary (browse), names dark machines in `pool.failed`, and never trusts a peer's verdict: a row only a peer holds reads `originVerified: "unknown"`. Wired in `AgentServer` with the route context.
|
|
14
|
+
3. `POST /mobile/chat/:topicId` with native conversations on (spec §4.2, §4.9 and the rollout section: the native flag gates every change to this route) no longer refuses invites, desktop codes or credential-shaped text: tokens are removed by exact match (`removeAuthorityTokens`, the same function the app route uses), the redacted text is what is stored (with a signed redaction attestation), ingested and mirrored, and the answer carries `redacted`/`removed`; credential-shaped text is delivered as written with `credentialShaped: true`, and the 📱 mirror shows it as `[credential hidden]` (`hideCredentialSpans`, built on `scrubForStore`). With native off, the route refuses exactly as before.
|
|
15
|
+
5. History rows (browse, sync, the device's Telegram history join) carry `originApprovedVia` (`telegram-dm` / `dashboard-pin`, from this machine's device-key ledger) when the verdict is `verified` or `redacted`, so the app can show "approved on dashboard" (§4.4, §4.6 display).
|
|
16
|
+
|
|
17
|
+
Gap 4 (the "📱 signed" tag) was verified present for app conversations (`appInboundDispatch.ts` adds it only for `verified`); the spec defines no such tag for Telegram-topic sessions, so none was added there. Slack rows in the list are absent because the Slack-from-the-app routes (spec increment 4) are not built; listing rows the app cannot open would be worse than omitting them.
|
|
18
|
+
|
|
19
|
+
Agent awareness: one new bullet in the Native app conversations section (pool reads, the single list cursor, `originApprovedVia`), and the Mobile operator chat content bullet rewritten; both reach existing agents through `migrateClaudeMd` (content-sniffed, idempotent).
|
|
20
|
+
|
|
21
|
+
Size: Tier 2 (spec-driven), 6 source files, about 300 lines plus tests. One review pass; commit attempts recorded in the PR.
|
|
22
|
+
|
|
23
|
+
## Decision-point inventory
|
|
24
|
+
|
|
25
|
+
- Telegram send preflight (`POST /mobile/chat/:topicId`) — modify — with native on, the three content refusals become exact-token removal plus a credential signal, as spec §4.2 already does for app conversations. Native off: pass-through (unchanged).
|
|
26
|
+
- Pool read merge — add — invariant: which rows a pool read returns is a deterministic merge; a peer's verdict is never adopted.
|
|
27
|
+
- `originApprovedVia` — add — display data read from the ledger; decides nothing.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 1. Over-block
|
|
32
|
+
|
|
33
|
+
The Telegram route now blocks less (native on): nothing is refused for its content. The list and browse reads refuse a `scope` value other than `pool` with 400 `invalid-scope` (as the Telegram history route already does); no existing client sends another value.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 2. Under-block
|
|
38
|
+
|
|
39
|
+
A credential typed into the app is now delivered to the session and kept in the Telegram history row as written (native on). That is the spec's explicit choice (§4.2: credential-shaped text is the operator's call, the app asks first; the server only signals). The mirror copy for people reading the topic hides it. An app build that skips its local check would let a credential through without asking; the server's `credentialShaped: true` and audit signal remain.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 3. Level-of-abstraction fit
|
|
44
|
+
|
|
45
|
+
Token removal reuses the one function the app route uses, so both routes treat the same text the same way. The pool fan-out reuses `poolPeerFetch` and the peer URL credential guard rather than adding a transport. The list order reuses the store's own keyset order rather than a second sort definition.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 4. Signal vs authority compliance
|
|
50
|
+
|
|
51
|
+
- [x] No — this change produces a signal consumed by an existing smart gate.
|
|
52
|
+
|
|
53
|
+
It removes blocking authority from a brittle detector (`detectOutboundCredential` used to refuse the message) and keeps it as a signal (`credentialShaped`), which the spec names as a judgment-candidate whose floor is "never refuses or alters". The only remaining alteration is exact-format token removal, which is structure, not judgment (spec Decision points table).
|
|
54
|
+
|
|
55
|
+
## 4b. Judgment-point check
|
|
56
|
+
|
|
57
|
+
No new static heuristic at a competing-signals decision point. The list order and the pool merge are invariants (total order, de-duplication by key).
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 5. Interactions
|
|
62
|
+
|
|
63
|
+
- **Shadowing:** the inbound core (`/internal/telegram-forward` handler, `trust` path) still refuses invite/desktop text with 422 `not-from-the-app`; after removal the text no longer carries either (the core's checks use the same `tlinv1:` regex and a leading `/desktop`, which the line-based removal covers), so the core's refusal stays as a backstop and is not reached for removed text. Tested end to end.
|
|
64
|
+
- **Double-fire:** none; the mirror is still best-effort after delivery.
|
|
65
|
+
- **Races:** pool reads are read-only. The list's duplicate collapse picks the newest copy; a lagging machine's older copy of the same conversation can reappear on a later page (the app already de-duplicates rows by id).
|
|
66
|
+
- **Feedback loops:** a peer's pool read is its plain read (no `scope`), so fan-out never recurses.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 6. External surfaces
|
|
71
|
+
|
|
72
|
+
- Response shapes gain fields (`pool`, `machine`, `originApprovedVia`, `redacted`, `removed`, `credentialShaped` on the Telegram send); nothing is removed. The list's `nextCursor` is non-null in more cases (now correct).
|
|
73
|
+
- Telegram: the 📱 mirror text changes only when a credential span is present (hidden) or a token was removed.
|
|
74
|
+
- Persistent state: the Telegram send stores the redacted text plus a redaction attestation instead of the original body when a token was removed (the same record shape app conversations use; verified `redacted`).
|
|
75
|
+
- Operator surface: no new operator action.
|
|
76
|
+
|
|
77
|
+
## 6b. Operator-surface quality
|
|
78
|
+
|
|
79
|
+
No operator surface (no dashboard or approval page touched) — not applicable.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 7. Multi-machine posture
|
|
84
|
+
|
|
85
|
+
**proxied-on-read** for the list and browse: `?scope=pool` merges every answering machine's plain read, with `pool.failed` for dark ones; the store itself already replicates (`appConversations` kind). A peer-only row's verdict reads `unknown` because only this machine's ledger is trusted (§4.8). `originApprovedVia` comes from this machine's replicated device-key ledger. No user-facing notices, no new durable state, no URLs.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 8. Rollback cost
|
|
90
|
+
|
|
91
|
+
Pure code change: revert and ship a patch. The CLAUDE.md lines added by the migration would remain on updated agents (accurate only while this code ships; a revert would need the old lines restored by a later migration). Turning `mobilePairing.native.enabled` off restores the Telegram route's refusals immediately.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Conclusion
|
|
96
|
+
|
|
97
|
+
The four gaps are closed against the spec with tests at unit and integration level; gap 4 needed no change. The one judgment worth a reader's attention is that credential-shaped text through the Telegram route is now delivered (native on), which is what the spec specifies and what app conversations already do.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Second-pass review (if required)
|
|
102
|
+
|
|
103
|
+
Not run: the brief for this change declared `--second-pass false`.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Evidence pointers
|
|
108
|
+
|
|
109
|
+
- `tests/unit/mobileConversationListPaging.test.ts` — order, cursor, lossless multi-source paging, duplicate collapse, mirror hiding.
|
|
110
|
+
- `tests/integration/mobile-native-conversations-gaps.test.ts` — 250 Telegram topics + 5 app conversations paged to completion; interleaved paging; pool list and browse with a live peer and a dark peer; Telegram token removal (`redacted`, token absent from disk), credential signal and hidden mirror, native-off refusals unchanged; `originApprovedVia` for Telegram- and dashboard-approved phones and absent on a failed record.
|
|
111
|
+
- `tests/unit/PostUpdateMigrator-appConversationsGaps.test.ts` — awareness lines for new and existing agents, idempotent.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Review follow-ups (independent review: CONCERN, all five addressed)
|
|
116
|
+
|
|
117
|
+
1. **Mirror leak through invisible characters.** `hideCredentialSpans` scrubbed the raw text only, while `detectOutboundCredential` also reads it with invisible characters removed, so a key split by a zero-width space was flagged but shown in the 📱 copy. Now the scrubbed copy is re-checked with the same wall; if the wall still sees a credential the whole copy is hidden as `[credential hidden]`. Tested for nine invisible characters (U+200B/C/D, U+2060, U+FEFF, U+00AD, U+034F, U+FE0F, U+202E) and end to end through the Telegram route.
|
|
118
|
+
2. **Desktop codes on a later line.** `DESKTOP_LINE_RE` allowed only space or tab before and after `/desktop`, so `/desktop\u00A0abc 123456` (and other non-ASCII whitespace) on a later line reached the session. Leading space and separator are now `[^\S\n]`, the core check's whitespace class minus newline. This function is shared with the app-conversation send route, so it removes more there too (only exact `/desktop` lines; `/desktops` and mid-line mentions are untouched, tested).
|
|
119
|
+
3. **Peer attachments.** Peer-only browse rows no longer carry the peer's attachment rows at all (their files live on that machine, which this machine cannot serve, and their fields were unchecked); the row says `filesOnOtherMachine: <count>`. `deliveryState` from a peer is restricted to the known values, else `unknown`.
|
|
120
|
+
4. **Peer times.** A peer's `lastActivity` (list) and `at` (browse) must be a strict ISO-8601 instant and are normalised with `toISOString()`; anything else drops the row, so a value like `"0"` can no longer become the cursor that ends paging early. A note: a peer row is sliced to `limit` before validation, so a peer returning invalid rows wastes its own slots, never another source's.
|
|
121
|
+
5. **Pool browse of a conversation not replicated here yet.** Spec 4.8 says the live pool read covers events not yet replicated, so `?scope=pool` now reads a conversation this machine has never seen from the peers (all rows `originVerified: "unknown"`). A conversation this machine knows is deleted stays 404; if no peer has it, 404; if a machine did not answer, 200 with `pool.failed`. Only a valid app conversation key is forwarded, and it is URL-encoded.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Class-Closure Declaration (display-only mirror)
|
|
126
|
+
|
|
127
|
+
- **Defect class:** `unbounded-self-action`
|
|
128
|
+
- **Closure:** `n/a` (negative declaration)
|
|
129
|
+
- **Reason:** one-shot user-driven action, not a self-triggered loop. The Telegram send route runs once per device request (rate-limited and idempotent through MobileChatRequestStore); this change only replaces its content refusals with token removal, and the pool reads are request-driven, read-only fan-outs.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Side-Effects Review — Later liveness loss after recovered autonomous run
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `window-run-liveness-later-loss`
|
|
4
|
+
**Date:** 2026-11-02
|
|
5
|
+
**Author:** Echo
|
|
6
|
+
**Second-pass reviewer:** `/root/outage_fallback_review` — concurred after equality-boundary correction
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Tier 1 narrow, reversible incident fix. One authority branch in `src/core/WindowRunLivenessAuthority.ts` now bounds a later at-risk episode from its own transition after a previously completed recovery; it preserves the original deadline while the first episode is still unresolved and never actuates recovery twice. Unit, authenticated HTTP, and production-boot lifecycle tests cover the boundary. This is a 4-code-file change at draft time, with one review round and no commit attempt yet.
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- `WindowRunLivenessAuthority.evaluateAndMaybeRecover()` — modified — chooses the deadline for an at-risk run with an existing recovery attempt.
|
|
15
|
+
- Recovery actuation, predicate sampling, active promotion, and terminal projection — passed through — unchanged.
|
|
16
|
+
|
|
17
|
+
## 1. Over-block
|
|
18
|
+
|
|
19
|
+
A later loss still fails if any predicate remains red for 15 minutes from its new at-risk transition. A transient later loss does not fail immediately only because the old deadline expired. The original unresolved loss still fails on its original ceiling. No additional legitimate input is rejected.
|
|
20
|
+
|
|
21
|
+
## 2. Under-block
|
|
22
|
+
|
|
23
|
+
Repeated short outages could re-enter active between episodes without another refresh. This is already possible whenever all five predicates become green; the change allows observation to distinguish distinct episodes, while the one recovery attempt remains spent. There is no new path to active with a red predicate. A later loss with no usable `atRiskSince` or a non-finite deadline fails closed.
|
|
24
|
+
|
|
25
|
+
## 3. Level-of-abstraction fit
|
|
26
|
+
|
|
27
|
+
The authority owns its recovery attempt, at-risk transition, and terminal deadline, so it is the right layer. The change uses those persisted fields and adds no competing monitor.
|
|
28
|
+
|
|
29
|
+
## 4. Signal vs authority compliance
|
|
30
|
+
|
|
31
|
+
Required reference: [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md).
|
|
32
|
+
|
|
33
|
+
- [x] No — this change maintains an existing server-owned authority's deterministic timing invariant, with all five independent predicate inputs still required for active.
|
|
34
|
+
|
|
35
|
+
The exact ordering of one completed attempt and a later transition is recorded by the authority. The new arithmetic cannot fabricate a green predicate or recovery receipt.
|
|
36
|
+
|
|
37
|
+
## 4b. Judgment-point check
|
|
38
|
+
|
|
39
|
+
No new static heuristic at a competing-signals decision point. The 15-minute ceiling is the configured existing invariant; this change selects the correct episode start for that invariant.
|
|
40
|
+
|
|
41
|
+
## 5. Interactions
|
|
42
|
+
|
|
43
|
+
- **Shadowing:** active promotion still requires all predicates green before the deadline branch.
|
|
44
|
+
- **Double-fire:** the persisted `recoveryAttempt` continues to suppress a second recovery call.
|
|
45
|
+
- **Races:** the authority's existing mutation lock serializes tick and state transitions; `atRiskSince` is written before this branch.
|
|
46
|
+
- **Feedback loops:** a later recovered episode returns to active, clearing `atRiskSince`; a new red sample starts a new bounded interval.
|
|
47
|
+
|
|
48
|
+
## 6. External surfaces
|
|
49
|
+
|
|
50
|
+
Users may no longer receive a false terminal failure for a later brief outage; after 15 minutes of continuing failure, the same terminal notice is sent. The durable record shape and HTTP schema are unchanged. No new operator action, URL, credential, or message source is introduced.
|
|
51
|
+
|
|
52
|
+
## 6b. Operator-surface quality
|
|
53
|
+
|
|
54
|
+
No operator surface — not applicable.
|
|
55
|
+
|
|
56
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
57
|
+
|
|
58
|
+
**Machine-local by design:** the liveness authority is bound to the run's current executor and uses that executor's transcript heartbeat plus its server-owned window state. The existing ownership/binding path determines which machine samples and projects the result. This change emits the existing failure notice at the later episode's real deadline, holds the same durable state, and generates no URLs. Topic transfer requires the existing rebind proof; this change does not add a new transfer path.
|
|
59
|
+
|
|
60
|
+
## 8. Rollback cost
|
|
61
|
+
|
|
62
|
+
Revert the branch and ship a patch. No schema migration or state cleanup is needed. During rollback propagation, a recovered run could again fail immediately on a later brief loss.
|
|
63
|
+
|
|
64
|
+
## Conclusion
|
|
65
|
+
|
|
66
|
+
The incident sequence is reproducible from the production authority record: first recovery returned active, then a later red sample was compared with the old ceiling. The patch preserves one recovery actuation and bounds each distinct at-risk interval. Unit tests and TypeScript pass; authenticated HTTP and production-boot tests are queued behind the host suite lane. Do not ship until those tests and the full required gates pass.
|
|
67
|
+
|
|
68
|
+
## Second-pass review (if required)
|
|
69
|
+
|
|
70
|
+
**Reviewer:** `/root/outage_fallback_review`
|
|
71
|
+
**Independent read of the artifact:** concur. The reviewer found the same-millisecond boundary in the first pass; I changed `>` to `>=` and added a test where recovery takes 14 minutes and the later loss is sampled in the completion millisecond. The second pass found no further blocker, subject to pending HTTP/E2E and full gates.
|
|
72
|
+
|
|
73
|
+
## Evidence pointers
|
|
74
|
+
|
|
75
|
+
- `.instar/autonomous/130742-evidence.md` — two live terminal incidents and the third run's admission receipt.
|
|
76
|
+
- `tests/unit/window-run-liveness-authority.test.ts` — focused unit receipt, 56 tests passed.
|
|
77
|
+
- `tests/integration/window-run-liveness-routes.test.ts` and `tests/e2e/window-run-liveness-production-wiring.test.ts` — added, queued.
|
|
78
|
+
|
|
79
|
+
## Class-Closure Declaration (display-only mirror)
|
|
80
|
+
|
|
81
|
+
No new self-action emit is introduced: the existing recovery attempt remains spent and the changed branch only chooses the at-risk deadline. The targeted unit and production-boot cases are the regression ratchet for the expired-prior-deadline defect. The `unbounded-self-action` class is not newly implicated by this diff; no agent-authored artifact is being corrected.
|
package/upgrades/1.3.1600.md
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# Upgrade Guide — vNEXT
|
|
2
|
-
|
|
3
|
-
<!-- assembled-by: assemble-next-md -->
|
|
4
|
-
<!-- bump: patch -->
|
|
5
|
-
|
|
6
|
-
## What Changed
|
|
7
|
-
|
|
8
|
-
New dev-gated server service (`docs/specs/ecosystem-idea-pipeline.md`). Each week it reviews a
|
|
9
|
-
watchlist of similar projects: trusted code fetches and reads, and AI services return closed-schema
|
|
10
|
-
judgments. Each idea is checked against our source and specs by two blind reads, reviewed against the
|
|
11
|
-
Instar 2.0 purpose, classed by what it would change, and ranked into bands. The default outcome is no
|
|
12
|
-
change. A framework reads outside text only after two must-fail file-read probes through the same call
|
|
13
|
-
path real reviews use; to pass them, `IntelligenceOptions.noLocalTools` and
|
|
14
|
-
`reviewerHardening.noTools` ask Codex and Claude for calls with no local tools. The default path of every
|
|
15
|
-
other caller is unchanged. `scripts/lint-ecosystem-adoption.mjs` joins the lint chain: it fails a default
|
|
16
|
-
change for a tagged spec unless `docs/proofs/<idea>.json` passes, and no spec is tagged yet.
|
|
17
|
-
|
|
18
|
-
## What to Tell Your User
|
|
19
|
-
|
|
20
|
-
If you develop your own project, I can check similar projects each week and show you ranked ideas, each
|
|
21
|
-
checked against your code. It only gives advice: it changes nothing and contacts nobody. It is off unless
|
|
22
|
-
you ask for it.
|
|
23
|
-
|
|
24
|
-
## Summary of New Capabilities
|
|
25
|
-
|
|
26
|
-
- `GET /ecosystem-ideas`, `GET /ecosystem-ideas/:id/evidence`, and PIN-gated decisions through `POST /ecosystem-ideas/plan` and `/commit`.
|
|
27
|
-
- The console's Work group gains an Ideas page.
|
|
28
|
-
|
|
29
|
-
## Evidence
|
|
30
|
-
|
|
31
|
-
Live on this machine on 2026-11-01: without the no-tools form the capability probe refused Codex (it read
|
|
32
|
-
the planted file), and Claude's call stopped when it tried a tool. With the form, both frameworks passed
|
|
33
|
-
both probes. Tests: fixture runs on a real clone hold a hostile CLAUDE.md and a marker-writing script;
|
|
34
|
-
the planted idea is found, no marker is written, and the same run with file reads disabled finds nothing.
|