instar 1.3.1621 → 1.3.1623
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/server.d.ts.map +1 -1
- package/dist/commands/server.js +4 -2
- package/dist/commands/server.js.map +1 -1
- package/dist/core/AppConversationService.d.ts +89 -4
- package/dist/core/AppConversationService.d.ts.map +1 -1
- package/dist/core/AppConversationService.js +312 -23
- package/dist/core/AppConversationService.js.map +1 -1
- package/dist/core/AppConversationStore.d.ts +21 -2
- package/dist/core/AppConversationStore.d.ts.map +1 -1
- package/dist/core/AppConversationStore.js +38 -1
- package/dist/core/AppConversationStore.js.map +1 -1
- package/dist/core/AppConversationsReplicatedStore.js +1 -1
- package/dist/core/AppConversationsReplicatedStore.js.map +1 -1
- package/dist/core/AppMediaStore.d.ts.map +1 -1
- package/dist/core/AppMediaStore.js +7 -0
- package/dist/core/AppMediaStore.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +3 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +28 -7
- package/dist/core/PostUpdateMigrator.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/feedback-factory/execute/FeedbackExecutorService.d.ts.map +1 -1
- package/dist/feedback-factory/execute/FeedbackExecutorService.js +13 -3
- package/dist/feedback-factory/execute/FeedbackExecutorService.js.map +1 -1
- package/dist/feedback-factory/execute/reviewGate.d.ts +3 -2
- package/dist/feedback-factory/execute/reviewGate.d.ts.map +1 -1
- package/dist/feedback-factory/execute/reviewGate.js +3 -2
- package/dist/feedback-factory/execute/reviewGate.js.map +1 -1
- package/dist/monitoring/GreenPrAutoMerger.d.ts.map +1 -1
- package/dist/monitoring/GreenPrAutoMerger.js +11 -1
- package/dist/monitoring/GreenPrAutoMerger.js.map +1 -1
- package/dist/monitoring/MergeRunner.d.ts +4 -5
- package/dist/monitoring/MergeRunner.d.ts.map +1 -1
- package/dist/monitoring/MergeRunner.js +2 -2
- package/dist/monitoring/MergeRunner.js.map +1 -1
- package/dist/native/instar-desktop-host +0 -0
- package/dist/scaffold/templates.js +1 -1
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +8 -3
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/mobileConversationRoutes.d.ts +6 -1
- package/dist/server/mobileConversationRoutes.d.ts.map +1 -1
- package/dist/server/mobileConversationRoutes.js +16 -2
- package/dist/server/mobileConversationRoutes.js.map +1 -1
- package/package.json +1 -1
- package/scripts/safe-merge.mjs +65 -67
- package/src/data/builtin-manifest.json +20 -20
- 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/scaffold/templates.ts +1 -1
- package/upgrades/1.3.1622.md +21 -0
- package/upgrades/1.3.1623.md +34 -0
- package/upgrades/side-effects/feedback-ci-floor-merge.md +82 -0
- package/upgrades/side-effects/voice-transcript-event.eli16.md +17 -0
- package/upgrades/side-effects/voice-transcript-event.md +136 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-11-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-11-03T11:42:07.490Z",
|
|
5
|
+
"instarVersion": "1.3.1623",
|
|
6
6
|
"entryCount": 206,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"domain": "identity",
|
|
12
12
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
13
13
|
"installedPath": ".instar/hooks/instar/session-start.sh",
|
|
14
|
-
"contentHash": "
|
|
14
|
+
"contentHash": "4ef8ef35eaf7ead8cd4b8302511ab47ca52862ee81827e6fbe6ce88dd6e38c5a",
|
|
15
15
|
"since": "2025-01-01"
|
|
16
16
|
},
|
|
17
17
|
"hook:dangerous-command-guard": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"domain": "safety",
|
|
21
21
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
22
22
|
"installedPath": ".instar/hooks/instar/dangerous-command-guard.sh",
|
|
23
|
-
"contentHash": "
|
|
23
|
+
"contentHash": "4ef8ef35eaf7ead8cd4b8302511ab47ca52862ee81827e6fbe6ce88dd6e38c5a",
|
|
24
24
|
"since": "2025-01-01"
|
|
25
25
|
},
|
|
26
26
|
"hook:grounding-before-messaging": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"domain": "safety",
|
|
30
30
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
31
31
|
"installedPath": ".instar/hooks/instar/grounding-before-messaging.sh",
|
|
32
|
-
"contentHash": "
|
|
32
|
+
"contentHash": "4ef8ef35eaf7ead8cd4b8302511ab47ca52862ee81827e6fbe6ce88dd6e38c5a",
|
|
33
33
|
"since": "2025-01-01"
|
|
34
34
|
},
|
|
35
35
|
"hook:compaction-recovery": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"domain": "identity",
|
|
39
39
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
40
40
|
"installedPath": ".instar/hooks/instar/compaction-recovery.sh",
|
|
41
|
-
"contentHash": "
|
|
41
|
+
"contentHash": "4ef8ef35eaf7ead8cd4b8302511ab47ca52862ee81827e6fbe6ce88dd6e38c5a",
|
|
42
42
|
"since": "2025-01-01"
|
|
43
43
|
},
|
|
44
44
|
"hook:external-operation-gate": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"domain": "safety",
|
|
48
48
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
49
49
|
"installedPath": ".instar/hooks/instar/external-operation-gate.js",
|
|
50
|
-
"contentHash": "
|
|
50
|
+
"contentHash": "4ef8ef35eaf7ead8cd4b8302511ab47ca52862ee81827e6fbe6ce88dd6e38c5a",
|
|
51
51
|
"since": "2025-01-01"
|
|
52
52
|
},
|
|
53
53
|
"hook:deferral-detector": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"domain": "safety",
|
|
57
57
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
58
58
|
"installedPath": ".instar/hooks/instar/deferral-detector.js",
|
|
59
|
-
"contentHash": "
|
|
59
|
+
"contentHash": "4ef8ef35eaf7ead8cd4b8302511ab47ca52862ee81827e6fbe6ce88dd6e38c5a",
|
|
60
60
|
"since": "2025-01-01"
|
|
61
61
|
},
|
|
62
62
|
"hook:self-stop-guard": {
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"domain": "coherence",
|
|
66
66
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
67
67
|
"installedPath": ".instar/hooks/instar/self-stop-guard.js",
|
|
68
|
-
"contentHash": "
|
|
68
|
+
"contentHash": "4ef8ef35eaf7ead8cd4b8302511ab47ca52862ee81827e6fbe6ce88dd6e38c5a",
|
|
69
69
|
"since": "2025-01-01"
|
|
70
70
|
},
|
|
71
71
|
"hook:post-action-reflection": {
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"domain": "evolution",
|
|
75
75
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
76
76
|
"installedPath": ".instar/hooks/instar/post-action-reflection.js",
|
|
77
|
-
"contentHash": "
|
|
77
|
+
"contentHash": "4ef8ef35eaf7ead8cd4b8302511ab47ca52862ee81827e6fbe6ce88dd6e38c5a",
|
|
78
78
|
"since": "2025-01-01"
|
|
79
79
|
},
|
|
80
80
|
"hook:external-communication-guard": {
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"domain": "safety",
|
|
84
84
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
85
85
|
"installedPath": ".instar/hooks/instar/external-communication-guard.js",
|
|
86
|
-
"contentHash": "
|
|
86
|
+
"contentHash": "4ef8ef35eaf7ead8cd4b8302511ab47ca52862ee81827e6fbe6ce88dd6e38c5a",
|
|
87
87
|
"since": "2025-01-01"
|
|
88
88
|
},
|
|
89
89
|
"hook:telegram-origin-guard": {
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"domain": "coherence",
|
|
102
102
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
103
103
|
"installedPath": ".instar/hooks/instar/scope-coherence-collector.js",
|
|
104
|
-
"contentHash": "
|
|
104
|
+
"contentHash": "4ef8ef35eaf7ead8cd4b8302511ab47ca52862ee81827e6fbe6ce88dd6e38c5a",
|
|
105
105
|
"since": "2025-01-01"
|
|
106
106
|
},
|
|
107
107
|
"hook:scope-coherence-checkpoint": {
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"domain": "coherence",
|
|
111
111
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
112
112
|
"installedPath": ".instar/hooks/instar/scope-coherence-checkpoint.js",
|
|
113
|
-
"contentHash": "
|
|
113
|
+
"contentHash": "4ef8ef35eaf7ead8cd4b8302511ab47ca52862ee81827e6fbe6ce88dd6e38c5a",
|
|
114
114
|
"since": "2025-01-01"
|
|
115
115
|
},
|
|
116
116
|
"hook:free-text-guard": {
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"domain": "safety",
|
|
120
120
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
121
121
|
"installedPath": ".instar/hooks/instar/free-text-guard.sh",
|
|
122
|
-
"contentHash": "
|
|
122
|
+
"contentHash": "4ef8ef35eaf7ead8cd4b8302511ab47ca52862ee81827e6fbe6ce88dd6e38c5a",
|
|
123
123
|
"since": "2025-01-01"
|
|
124
124
|
},
|
|
125
125
|
"hook:claim-intercept": {
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
"domain": "coherence",
|
|
129
129
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
130
130
|
"installedPath": ".instar/hooks/instar/claim-intercept.js",
|
|
131
|
-
"contentHash": "
|
|
131
|
+
"contentHash": "4ef8ef35eaf7ead8cd4b8302511ab47ca52862ee81827e6fbe6ce88dd6e38c5a",
|
|
132
132
|
"since": "2025-01-01"
|
|
133
133
|
},
|
|
134
134
|
"hook:claim-intercept-response": {
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
"domain": "coherence",
|
|
138
138
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
139
139
|
"installedPath": ".instar/hooks/instar/claim-intercept-response.js",
|
|
140
|
-
"contentHash": "
|
|
140
|
+
"contentHash": "4ef8ef35eaf7ead8cd4b8302511ab47ca52862ee81827e6fbe6ce88dd6e38c5a",
|
|
141
141
|
"since": "2025-01-01"
|
|
142
142
|
},
|
|
143
143
|
"hook:stop-gate-router": {
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
"domain": "safety",
|
|
147
147
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
148
148
|
"installedPath": ".instar/hooks/instar/stop-gate-router.js",
|
|
149
|
-
"contentHash": "
|
|
149
|
+
"contentHash": "4ef8ef35eaf7ead8cd4b8302511ab47ca52862ee81827e6fbe6ce88dd6e38c5a",
|
|
150
150
|
"since": "2025-01-01"
|
|
151
151
|
},
|
|
152
152
|
"hook:auto-approve-permissions": {
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
"domain": "safety",
|
|
156
156
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
157
157
|
"installedPath": ".instar/hooks/instar/auto-approve-permissions.js",
|
|
158
|
-
"contentHash": "
|
|
158
|
+
"contentHash": "4ef8ef35eaf7ead8cd4b8302511ab47ca52862ee81827e6fbe6ce88dd6e38c5a",
|
|
159
159
|
"since": "2025-01-01"
|
|
160
160
|
},
|
|
161
161
|
"job:health-check": {
|
|
@@ -1563,7 +1563,7 @@
|
|
|
1563
1563
|
"type": "subsystem",
|
|
1564
1564
|
"domain": "server",
|
|
1565
1565
|
"sourcePath": "src/server/AgentServer.ts",
|
|
1566
|
-
"contentHash": "
|
|
1566
|
+
"contentHash": "a71a8206bb0642f329a162435b6ba75f157cf79fcc33090faa81584d8dc45775",
|
|
1567
1567
|
"since": "2025-01-01"
|
|
1568
1568
|
},
|
|
1569
1569
|
"subsystem:session-manager": {
|
|
@@ -1595,7 +1595,7 @@
|
|
|
1595
1595
|
"type": "subsystem",
|
|
1596
1596
|
"domain": "updates",
|
|
1597
1597
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
1598
|
-
"contentHash": "
|
|
1598
|
+
"contentHash": "4ef8ef35eaf7ead8cd4b8302511ab47ca52862ee81827e6fbe6ce88dd6e38c5a",
|
|
1599
1599
|
"since": "2025-01-01"
|
|
1600
1600
|
},
|
|
1601
1601
|
"subsystem:scheduler": {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedFrom": "source-tree",
|
|
4
4
|
"registrySha256": "db01d9f92c6aa26ca903702896b41be53722982ed07d27633237bb254c214290",
|
|
5
|
-
"packageVersion": "1.3.
|
|
5
|
+
"packageVersion": "1.3.1623",
|
|
6
6
|
"guards": [
|
|
7
7
|
{
|
|
8
8
|
"ref": "docs/audits/phase-b/f10-triage.md",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"sha256": "
|
|
2
|
+
"sha256": "da66636c285f0533ff9522714ba30fc0cb093bdc46b67a27cc2f6046d08bbfd4",
|
|
3
3
|
"registrySha256": "db01d9f92c6aa26ca903702896b41be53722982ed07d27633237bb254c214290",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1623"
|
|
5
5
|
}
|
|
@@ -1170,7 +1170,7 @@ ${READINESS_STALL_NOTICE_BULLET}
|
|
|
1170
1170
|
- **When to use** (PROACTIVE): "what are we working on from feedback?" → the queue; "why was this report ignored / held?" → the summary's reasons (and the item's row in the queue/summary); \`authority: awaiting-approval\` → send the operator to the dashboard Feedback Drain tab, Triage section, for the one PIN tap. Turning ignores live goes through \`POST /feedback-factory/triage/plan\` (\`{"action":"ignore-live"}\`) and the operator's PIN on the dashboard — never a chat confirmation.
|
|
1171
1171
|
|
|
1172
1172
|
**Feedback executor (operated feedback factory)** — The executor takes the highest-ranked **work** items and turns each into a pull request. Each attempt runs a Claude Code build session locked in an OS sandbox (no network, no credentials, no reads outside its own copy of the code), proven by a must-fail/must-succeed canary before every attempt; the session reproduces the problem with a failing test and fixes it (or drafts a spec when the item needs a design). Trusted code then re-checks the fix in the sandbox (fails before, passes after, lint and related tests pass), refuses changes to build tooling or anything that looks like a secret, and pushes from its own clone to a FORK (\`feedbackFactory.execute.publishRepo\`, \`owner/name\`), opening a cross-repository PR with the \`hold\` label — CI on a fork PR gets no repository secrets, and without a fork (or with the canonical repository itself) nothing is published. It merges ONLY the exact version the repository owner approved on GitHub (\`safe-merge --auto --match-head-commit\`), and refuses to run while this agent could itself act as that approver unless the operator accepted that once with the dashboard PIN. A merged fix is verified only after it ships in a release and the reports stay quiet for 30 days. Dev-gated (\`feedbackFactory.execute\`), dry-run until \`feedbackFactory.execute.dryRun\` is false; 503 without a source checkout.
|
|
1173
|
-
- Status: \`curl -H "Authorization: Bearer $AUTH" http://localhost:${port}/feedback-factory/execute/status\` → \`{ available, reason, dryRun, counts, live, openPrs, startsToday, limits, canary, deps, attempts }\` (\`reason\`: \`approver-not-independent\` / \`
|
|
1173
|
+
- Status: \`curl -H "Authorization: Bearer $AUTH" http://localhost:${port}/feedback-factory/execute/status\` → \`{ available, reason, dryRun, counts, live, openPrs, startsToday, limits, canary, deps, attempts }\` (\`reason\`: \`approver-not-independent\` / \`profile-unenforceable\` / \`deps-unavailable\` / \`publish-fork-unset\` / \`dry-run\` / \`no-source-repo\`). Native GitHub auto-merge need not be enabled; the reviewed head merges only after the complete CI floor passes.
|
|
1174
1174
|
- Operator instructions given in conversation: stop an item's attempts (also disarms an armed merge) \`curl -X POST -H "Authorization: Bearer $AUTH" -H 'X-Instar-Request: 1' -H 'Content-Type: application/json' http://localhost:${port}/feedback-factory/execute/stop -d '{"initiativeId":"..."}'\`; let a parked item be tried again \`.../feedback-factory/execute/release\` with the same body; withdraw the approver acceptance \`.../feedback-factory/execute/revoke-acceptance\` (no body).
|
|
1175
1175
|
- **When to use** (PROACTIVE): "is anything being fixed from feedback?" → the status and the queue's \`executionState\`/\`prLink\`; \`reason: approver-not-independent\` → send the operator to the dashboard Feedback Drain tab, Triage section, to accept or decline with the PIN (\`POST /feedback-factory/triage/plan\` \`{"action":"accept-approver-dependence"}\` renders it); a fix held as \`needs-review-secret-shape\` is published only through the same PIN plan/commit (\`{"action":"publish-secret-shape","attemptId":"..."}\`) — never a chat confirmation. You never merge an executor PR yourself.
|
|
1176
1176
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Feedback fixes and the green PR watcher now wait for the complete, producer-bound GitHub CI floor before merging. The merge command targets the exact verified head immediately after that check; it no longer leaves native auto-merge armed under a weaker repository rule. Queued or not-yet-registered checks make the feedback executor retry without spending its refusal allowance. That wait has a durable three-hour limit per attempt, followed by the existing one-hour retry path. The executor no longer requires native GitHub auto-merge to be enabled.
|
|
9
|
+
|
|
10
|
+
## What to Tell Your User
|
|
11
|
+
|
|
12
|
+
A feedback PR stays open while CI is queued. When the full suite is green, Echo merges the reviewed commit. If checks never arrive, the executor eventually holds the work and reports the merge problem instead of calling a missing suite green.
|
|
13
|
+
|
|
14
|
+
## Summary of New Capabilities
|
|
15
|
+
|
|
16
|
+
- Feedback pull requests wait for all relevant GitHub checks and merge only the reviewed head after those checks pass.
|
|
17
|
+
- Queued checks are retried for up to three hours per attempt; the executor allows its existing retry before reporting a merge hold.
|
|
18
|
+
|
|
19
|
+
## Evidence
|
|
20
|
+
|
|
21
|
+
Focused unit, integration, and E2E tests cover pending checks, a Vercel-first registration window, the full producer-bound floor, head-pinned merging, and the bounded executor retry.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
- A voice note sent from the Instar iPhone app without a phone transcript is still transcribed on the server with the same provider Telegram voice notes use (`GROQ_API_KEY` or `OPENAI_API_KEY`). The transcript is now saved with the conversation as its own replicated `transcript` event, instead of living only in the line the session received.
|
|
9
|
+
- Before the transcript is saved or delivered it gets the same treatment as a phone transcript: the pairing service's inbound scan runs over it (a dictated pending pairing code declines that pairing), and pairing invites and desktop-code lines are removed.
|
|
10
|
+
- The transcript is unsigned and kept outside the signed message, so it never changes `originVerified`. History and sync show it on the file as `serverTranscript` (`text`, `provider`, `at`, `machineId`, `signed: false`, `late` when it was late). The phone's own `transcript` field is unchanged.
|
|
11
|
+
- The session still waits at most 60 seconds. If the transcript is not ready by then, the line reads `(transcript not ready after 60 s; …)`. When it finishes, it is saved marked `late`, and an app conversation's live session gets one `[voice transcript ready: <path>] <text>` line (or `[voice transcript unavailable: <path>] (…reason…)` if it failed). With no live session, that line waits for the next delivery's context.
|
|
12
|
+
- A voice note that reached this machine only after the message was delivered (the `[file now available: <path>]` line) is transcribed when it lands, and the transcript rides on that line.
|
|
13
|
+
- At most 3 provider calls run at once per process. A note still waiting for a slot when its 10-minute ceiling passes ends as "transcription unavailable: timed out waiting".
|
|
14
|
+
- Invite and desktop-code removal also runs when a stored transcript is read (session lines, the next delivery's context, history, sync), so a transcript a peer stored without scrubbing cannot carry one.
|
|
15
|
+
- A transcript reaches the session once: a "now available" line kept for later and a late transcript line never both carry it.
|
|
16
|
+
- Each voice note is transcribed at most once per machine. A failure is recorded with its reason and never retried; a transcription interrupted by a restart reads "the server restarted before the transcript finished". A transcript another machine already made is reused.
|
|
17
|
+
- The provider call itself may now run up to 10 minutes (it was cut at 60 seconds, which made a late transcript impossible).
|
|
18
|
+
- Older peers do not know the `transcript` event kind: they refuse it and count it, exactly as they refuse `media-message`.
|
|
19
|
+
|
|
20
|
+
## What to Tell Your User
|
|
21
|
+
|
|
22
|
+
Voice notes you send from the app without a phone transcript now keep their transcript: it is saved with the message and shows in the app, marked as made by the agent rather than signed by your phone. If transcribing takes longer than a minute, I get your voice note right away and the words follow a moment later. If transcription fails or no transcription service is set up, I tell you why, and I can still listen to the file.
|
|
23
|
+
|
|
24
|
+
## Summary of New Capabilities
|
|
25
|
+
|
|
26
|
+
- The `transcript` app-conversation event kind (replicated with `appConversations`).
|
|
27
|
+
- `serverTranscript` on voice attachments in app history and sync.
|
|
28
|
+
- Late transcript follow-up lines in app conversations.
|
|
29
|
+
|
|
30
|
+
## Evidence
|
|
31
|
+
|
|
32
|
+
- Unit (`tests/unit/app-voice-transcript-event.test.ts`, 41 tests, including the second-pass findings: the 3-call limit under a burst of 8, queued-past-ceiling, peer transcripts scrubbed on read, and no duplicate delivery in either direction): the event validator on both sides of every field rule; the replicated envelope round trip with nothing dropped; an older peer's validator refusing and counting the record while the current one accepts it; one winning transcript per attachment in either arrival order; scrub and scan order; cut-with-note past the store limit; one transcription per attachment, failures never retried, a restart-interrupted run read as a failure; late transcripts stored `late` with exactly one follow-up line, or a note for the next delivery; a Telegram conversation stored with no follow-up promised; notes that land later transcribed and carried on the "now available" line; the signed record byte-for-byte unchanged.
|
|
33
|
+
- Integration (`tests/integration/mobile-native-media.test.ts`, 5 new tests, one with a peer-authored transcript carrying an invite): through the production device authenticator, a voice note without a phone transcript is stored as a scrubbed, scanned transcript event, shown as `serverTranscript` in history and sync, with `originVerified: "verified"`; a phone transcript is never replaced; a late transcript arrives as one follow-up line and history shows `late: true`; a failing provider gives an honest line and stores nothing.
|
|
34
|
+
- E2E (`tests/e2e/mobile-native-media-alive.test.ts`, 1 new test): an AgentServer booted on a development agent with `GROQ_API_KEY` set calls the real provider path once, delivers the scrubbed transcript, and the paired phone's history shows the unsigned `serverTranscript` beside a verified signature.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Side-Effects Review — Feedback CI floor merge
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `feedback-ci-floor-merge`
|
|
4
|
+
**Date:** 2026-11-03
|
|
5
|
+
**Author:** Echo
|
|
6
|
+
**Second-pass reviewer:** feedback_gate_review
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
`scripts/safe-merge.mjs` now defers its `--auto` route until the full producer-bound Actions floor is green and then calls the immediate head-pinned merge. `FeedbackExecutorService` retries pending checks under a durable deadline and removes the obsolete native auto-merge prerequisite. The CLAUDE.md template and migration both explain the revised behavior. `GreenPrAutoMerger` treats the same deferral as backpressure, not a failed ladder attempt. Tests cover all three layers. The site delivery-checks guide explains the waiting states and restores the documentation coverage floor after the repository's recent class growth.
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- `safe-merge --auto` — modify — pending, red, missing, and green CI classification before a merge.
|
|
15
|
+
- `FeedbackExecutorService` — modify — pending retry and three-hour timeout.
|
|
16
|
+
- `FeedbackExecutorService.refreshAvailability` — modify — native auto-merge setting no longer gates an immediate CI-floor merge.
|
|
17
|
+
- `GreenPrAutoMerger.recordOutcome` — modify — pending does not consume ladder attempts.
|
|
18
|
+
|
|
19
|
+
## 1. Over-block
|
|
20
|
+
|
|
21
|
+
A legitimate PR whose GitHub Actions workflow is delayed more than three hours spends a merge refusal and may eventually be held. It remains recoverable through the existing retry and hold path. The executor no longer refuses solely because native auto-merge is disabled; it uses the immediate CI-floor merge path. Wrong-producer checks refuse even if a legitimate run later arrives; the next retry can pass after the correct run appears.
|
|
22
|
+
|
|
23
|
+
## 2. Under-block
|
|
24
|
+
|
|
25
|
+
A check rerun may turn red after the final read and before the merge command. GitHub enforces its configured contexts during the command, but its configured floor can be weaker. The head pin prevents a new commit from merging; the check-state race remains a short window. No durable native auto-merge request is left armed.
|
|
26
|
+
|
|
27
|
+
## 3. Level-of-abstraction fit
|
|
28
|
+
|
|
29
|
+
The merger owns the final CI predicate and the act-time head pin. The executor owns its durable retry clock. The watcher owns its ladder classification. Each decision stays at the component that has its required state.
|
|
30
|
+
|
|
31
|
+
## 4. Signal vs authority compliance
|
|
32
|
+
|
|
33
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
34
|
+
|
|
35
|
+
- [x] No — the CI floor is a deterministic safety invariant on an irreversible merge, with producer-bound source checks; pending is a signal to the existing retry authority.
|
|
36
|
+
|
|
37
|
+
## 4b. Judgment-point check (Judgment Within Floors standard)
|
|
38
|
+
|
|
39
|
+
The merge floor is an enumerable safety invariant: every named CI context from the pinned producer must be successful for the exact head. The retry deadline is a bounded operational policy, not a competing-signals judgment.
|
|
40
|
+
|
|
41
|
+
## 5. Interactions
|
|
42
|
+
|
|
43
|
+
The green PR watcher still recognizes historical armed requests and reconciles them. The new path produces no new armed requests. Feedback hold-label removal still precedes the safe-merge call; a pending result leaves the PR open and the watcher also defers while CI is pending.
|
|
44
|
+
|
|
45
|
+
## 6. External surfaces
|
|
46
|
+
|
|
47
|
+
`safe-merge --capabilities` now advertises `ci-floor-sync-merge` and `pending-check-deferral`. `--auto` still parses for existing callers, but its action is a synchronous verified merge. The result line returns `deferred:checks-pending` for queued or not-yet-registered checks.
|
|
48
|
+
|
|
49
|
+
## 6b. Operator-surface quality (Operator-Surface Quality standard)
|
|
50
|
+
|
|
51
|
+
The feedback row says why it is waiting. No operator action is requested while CI is merely queued. A timed-out feedback executor check goes through the existing merge-unavailable hold with one listed item. The green PR watcher retries pending checks without an executor-style hold deadline.
|
|
52
|
+
|
|
53
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
54
|
+
|
|
55
|
+
The head SHA and CI results are read from GitHub, common to all machines. The feedback row's deadline is in the existing execution store and follows its owner lease. The watcher uses its existing fenced lease and state ledger. No machine-local-only authority is added.
|
|
56
|
+
|
|
57
|
+
## 8. Rollback cost
|
|
58
|
+
|
|
59
|
+
No persistent schema migration is involved. Rolling back the script to its previous `--auto` behavior would reopen the weaker-floor merge risk for both the executor and the watcher. A safe rollback must first disable both autonomous merge paths or replace them with an immediate merge path that checks the full CI floor. Pending executor rows remain readable by an old binary, but retrying them with that binary is unsafe until its merge path is disabled.
|
|
60
|
+
|
|
61
|
+
## Conclusion
|
|
62
|
+
|
|
63
|
+
The change prevents merges based on a weaker GitHub rule and bounds delayed CI registration. The remaining read-to-merge check-state race is documented above.
|
|
64
|
+
|
|
65
|
+
## Second-pass review (if required)
|
|
66
|
+
|
|
67
|
+
**Reviewer:** feedback_gate_review
|
|
68
|
+
|
|
69
|
+
Concur with the review. The reviewer found and cleared the durable auto-arm, CI-registration, head-of-line, and rollback-description gaps after revisions. A later release-summary wording concern was corrected to say the three-hour window is per attempt and a retry precedes a hold; the reviewer reconfirmed concurrence. The final diff requires its exact-head tests and CI.
|
|
70
|
+
|
|
71
|
+
## Evidence pointers
|
|
72
|
+
|
|
73
|
+
- `tests/integration/safe-merge-no-checks-ran.test.ts`
|
|
74
|
+
- `tests/unit/feedback-factory/execute-service.test.ts`
|
|
75
|
+
- `tests/unit/green-pr-automerger.test.ts`
|
|
76
|
+
- `tests/e2e/feedback-execute-lifecycle.test.ts`
|
|
77
|
+
- `site/src/content/docs/features/feedback-delivery-checks.md`
|
|
78
|
+
- `upgrades/next/feedback-ci-floor-merge.md`
|
|
79
|
+
|
|
80
|
+
## Class-Closure Declaration (display-only mirror)
|
|
81
|
+
|
|
82
|
+
`defectClass: claim-vs-evidence`, `closure: guard`, `guardEvidence: { enforcementType: gate, citation: scripts/safe-merge.mjs, howCaught: the complete producer-bound CI floor is read before the head-pinned merge; pending checks return without a merge command }`.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Voice note transcripts are now saved (plain English)
|
|
2
|
+
|
|
3
|
+
When you record a voice note in the Instar iPhone app, the phone usually turns your speech into text by itself and sends that text along with the audio. Sometimes it can't: the language isn't supported on the phone, the recognizer fails, or the note is too long. In those cases the agent's computer does the transcription instead, using the same service it already uses for Telegram voice notes.
|
|
4
|
+
|
|
5
|
+
Until now, that computer-made text was handed to the agent's session once and then forgotten. It was not saved with the conversation, so the app never showed it, and another of your machines never saw it.
|
|
6
|
+
|
|
7
|
+
This change saves it. The text is stored next to the message as a separate record that says "the agent's computer made this". It is kept separate on purpose: your phone signs every message you send, so anyone can check later that the message really came from your phone. The phone never saw the computer-made text, so it must not look as if the phone signed it. The app shows it marked as coming from the agent, and your message still shows as signed.
|
|
8
|
+
|
|
9
|
+
Before the text is saved or shown to the agent, it gets the same safety cleaning as text from the phone. If you happened to say a pairing invite or a remote-desktop code out loud, it is removed, and a pending pairing whose code you dictated is cancelled.
|
|
10
|
+
|
|
11
|
+
Transcription sometimes takes a while. The agent does not wait more than a minute: it gets your voice note right away with a short note that the words will follow. When the text arrives, it is saved and the agent gets one extra line with it. If nobody is listening right then, that line is waiting for the agent the next time you write.
|
|
12
|
+
|
|
13
|
+
If the voice note itself was still being copied from another of your machines when the agent first heard about it, the computer transcribes it as soon as the file arrives and includes the text in the "your file is here now" line.
|
|
14
|
+
|
|
15
|
+
Each voice note is transcribed at most once on each computer. If it fails, the agent is told why in plain words and can still listen to the audio file; it does not keep trying over and over.
|
|
16
|
+
|
|
17
|
+
Older versions of Instar on your other machines don't know this new kind of record. They politely refuse it and count the refusal, and everything else keeps working.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Side-Effects Review — server voice transcripts as their own event
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `voice-transcript-event`
|
|
4
|
+
**Date:** `2026-11-03`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `required` (first pass raised four concerns, fixed below; the re-check is pending)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Finishes docs/specs/mobile-native-chat-media.md §4.5.1 (evolution action ACT-179). A voice note the app sends without a phone transcript was already transcribed at dispatch through the shared `VoiceTranscription` module; the text reached only the session's line. Now the server transcript is stored as its own replicated `{kind: "transcript"}` event (`src/core/AppConversationStore.ts`: kind, validator, a `transcripts` index table picking one winner per attachment, `transcriptFor`), the replicated envelope knows its fields (`src/core/AppConversationsReplicatedStore.ts`: `attachmentId`, `provider`, `late`), and `AppConversationService` (`src/core/AppConversationService.ts`) scrubs it (inbound scan, then exact-token removal, cut with a note past 16,000 characters), stores it, runs at most one transcription per attachment per machine (`voice_transcriptions` rows in the media index, `src/core/AppMediaStore.ts`), and handles the late case: a transcript finishing after the 60 s dispatch deadline is stored `late: true` and produces one follow-up line (or a `transcript_notes` row the next delivery's context carries). Voice notes resolved through `[file now available]` are transcribed when they land. History and sync show the stored transcript on the attachment as `serverTranscript` (`src/server/mobileConversationRoutes.ts` `attachmentRows`). The production wiring (`src/server/AgentServer.ts`, `src/commands/server.ts`) raises the provider's own timeout to 10 minutes, names the provider, and connects the pairing service's `scanInbound`. The CLAUDE.md "App conversation media" voice line is updated in the template and refreshed in place by `PostUpdateMigrator`.
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- `validateAppEvent` (event acceptance) — modify — accepts one new kind with strict field rules; every other kind unchanged.
|
|
15
|
+
- One-winner transcript index — add — earliest `at`, then lowest machine id. An invariant ordering, not a judgment.
|
|
16
|
+
- One-attempt-per-attachment ledger — add — a bound, not a competing-signals decision.
|
|
17
|
+
- Session line choice for a voice note — modify — stored transcript / running / recorded failure / pending after deadline. Enumerable states.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 1. Over-block
|
|
22
|
+
|
|
23
|
+
No block/allow surface. Nothing is refused that was accepted before: a send, a dispatch and a history read behave as before. The one new refusal is a malformed `transcript` event in the validator, which only this change produces.
|
|
24
|
+
|
|
25
|
+
One deliberate narrowing: a voice note whose transcription failed once (including "no provider configured") is never transcribed again on that machine, even if a provider is configured later. That is the requested bound (one transcription per attachment, never retried in a loop). The audio stays readable.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 2. Under-block
|
|
30
|
+
|
|
31
|
+
No block/allow surface. Gaps that remain, stated plainly:
|
|
32
|
+
|
|
33
|
+
- A transcript from a Telegram topic or Slack conversation continued from the app is stored, but no late follow-up line reaches those sessions (the follow-up injector only knows app conversation aliases; the same limit exists today for `[file now available]` there). The dispatch line says so ("saved with the message if it finishes") instead of promising a follow-up.
|
|
34
|
+
- Two machines can each transcribe the same attachment once (each holds its own attempt row). The index picks one winner deterministically, so every machine converges on the same text; the cost is at most one extra provider call per machine.
|
|
35
|
+
- Credential-shaped text in a server transcript is not removed (the phone-transcript path only flags it too). The session receives what was said.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 3. Level-of-abstraction fit
|
|
40
|
+
|
|
41
|
+
The transcript is stored through the existing conversation store and replicated through the existing `appConversations` kind; no new store or transport. Scrubbing reuses `removeAuthorityTokens` and the pairing service's `scanInbound`, the same primitives the phone transcript path uses. The provider path is the existing `transcribeAudioFile` (no new provider). The late follow-up reuses the existing app follow-up injector and the pending-context path that `[file now available]` already uses.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 4. Signal vs authority compliance
|
|
46
|
+
|
|
47
|
+
- [x] No — this change has no block/allow surface.
|
|
48
|
+
|
|
49
|
+
It records and delivers text. The inbound scan it calls already exists and has the same authority as on the phone-transcript path (declining a pending pairing whose code was dictated).
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 4b. Judgment-point check (Judgment Within Floors standard)
|
|
54
|
+
|
|
55
|
+
No new static heuristic at a competing-signals decision point. The winner rule is an ordering invariant (earliest, then machine id) chosen so arrival order cannot change the answer; the attempt ledger is a bound.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 5. Interactions
|
|
60
|
+
|
|
61
|
+
- **Shadowing:** the server transcript never replaces a phone transcript: a voice attachment with `transcript` set is never sent to the provider (unit and integration tests assert it).
|
|
62
|
+
- **Double-fire:** two concurrent deliveries of the same attachment join one in-process transcription and arm one follow-up (unit test "two deliveries racing"). A redelivery after the transcript is stored reuses it. The follow-up and the next delivery's note are exclusive: a note is written only when the follow-up was not delivered, and is deleted once a delivery acknowledges it.
|
|
63
|
+
- **Races:** the attempt row is claimed with `INSERT OR IGNORE` before the provider starts. A process that dies mid-call leaves a `running` row, which the next look reads as a failure ("the server restarted before the transcript finished") and never re-runs. The late flag is set by the deadline before the transcription settles, so the stored `late` marker matches what the session was told.
|
|
64
|
+
- **Feedback loops:** none. A stored transcript does not trigger anything; replication of a transcript event from a peer only updates the index.
|
|
65
|
+
- **Signatures:** the transcript is a separate event; the signed `media-message` is never rewritten, so `verifyRecordedOrigin` sees exactly the record it saw before (unit test compares the stored record byte for byte; integration and E2E read `originVerified: "verified"` with a stored transcript present).
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 6. External surfaces
|
|
70
|
+
|
|
71
|
+
- **Provider:** the call is unchanged except that it may now run up to 10 minutes before it is aborted (was 60 s). Still one call per attachment per machine.
|
|
72
|
+
- **Other machines:** transcript events replicate. Older peers refuse the unknown kind and count it (`store-schema-rejected`), exactly as for `media-message`; nothing breaks there, they simply do not show the transcript.
|
|
73
|
+
- **The app:** history and sync rows for voice attachments may carry a new `serverTranscript` object. Sync also returns `transcript` events; the app already ignores kinds it does not render (delivery and purge events have always been in that stream).
|
|
74
|
+
- **Persistent state:** two small tables in the media index (`voice_transcriptions`, `transcript_notes`) and one in the conversation index (`transcripts`, derived and rebuildable). Bounded: notes expire after 7 days, finished attempt rows after 30 days.
|
|
75
|
+
- **Operator surface:** no operator-facing action added.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 6b. Operator-surface quality (Operator-Surface Quality standard)
|
|
80
|
+
|
|
81
|
+
No operator surface — not applicable.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
86
|
+
|
|
87
|
+
- **Transcript events: replicated** through the existing `appConversations` replicated-store kind; every machine converges on the same winner.
|
|
88
|
+
- **Attempt ledger and late notes: machine-local by design.** The attempt row bounds this machine's own provider calls; the note is for this machine's session (the owner that delivered the message). Neither holds data another machine needs: the transcript itself replicates.
|
|
89
|
+
- **User-facing notices:** the follow-up line goes only to the conversation's session on the machine that owns the alias (`ownsAlias`), so one voice.
|
|
90
|
+
- **Durable state on transfer:** a moved conversation's new owner reads the replicated transcript; a pending late note stays with the old owner and expires after 7 days (the transcript is still in history).
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 8. Rollback cost
|
|
95
|
+
|
|
96
|
+
Pure code change: revert and ship a patch. After rollback, an older build refuses stored `transcript` events in its validator, so they read as absent (the session line and history fall back to the previous behavior); no migration needed. The three new tables are unused and harmless. The CLAUDE.md voice line would read slightly ahead of the code until the next refresh.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Conclusion
|
|
101
|
+
|
|
102
|
+
The change stores what the server already produced, keeps it outside the signed record, bounds the provider to one call per attachment, and makes every failure visible as a plain reason. The review moved two things: the provider's own timeout (60 s made the late path impossible) and the platform-conversation line, which no longer promises a follow-up it cannot deliver. Clear to ship.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Second-pass review
|
|
107
|
+
|
|
108
|
+
**Reviewer:** second-pass reviewer (via the coordinator)
|
|
109
|
+
**Independent read of the artifact: concern — fixed; re-check concurred (2026-11-03 03:24 PST)**
|
|
110
|
+
|
|
111
|
+
Concerns raised, and what was done:
|
|
112
|
+
|
|
113
|
+
- **Concurrency.** Nothing bounded how many provider calls one process could run at once; a burst of voice notes would start one call each. **Fixed:** a per-process limit of 3 provider calls (`VOICE_TRANSCRIBE_CONCURRENCY`, `limitedTranscribe` in `src/core/AppConversationService.ts`). Time spent queued counts toward the same 10-minute ceiling: a call still queued at the ceiling ends as "transcription unavailable: timed out waiting" and never reaches the provider; a running call is given only the remaining time (passed to the provider as its own timeout) and reads "the transcription service timed out" at the ceiling. A slot is held until the provider call itself settles, so at most 3 ever run. Tests: 8 notes fired at once peak at exactly 3 concurrent calls and all settle; with a limit of 1 and a 60 ms ceiling, the queued call times out waiting without a provider call and the running one times out, both recorded and not retried.
|
|
114
|
+
- **Peer transcripts.** Scrubbing ran only where this machine wrote the transcript; a peer that skipped it (an older or misbehaving build) could replicate a transcript carrying a `tlinv1:` invite or a `/desktop` code line straight to a session or the phone. **Fixed:** the exact-token removal (`transcriptForDelivery`, i.e. `removeAuthorityTokens`) now also runs on READ in every path that hands a stored transcript out: `transcription()` (the dispatch and "now available" lines), `pendingContext` (both the "now available" rows and the late notes), the late line, `serverTranscript` in history, and the `transcript` event's text in sync. Tests: a peer-authored transcript with an invite and a desktop line, stored as written, never reaches a session line, a pending-context line, history or sync unscrubbed (unit and integration).
|
|
115
|
+
- **Duplicate delivery.** With no live session, a "now available" row kept for the next delivery could carry the transcript AND a separate late note could say it again; and if the late line reached a live session after the row was kept, the row would repeat it later. **Fixed:** a late note is not written when a kept "now available" row will carry the transcript; `pendingContext` drops a note whose transcript a row already carries; a transcript that reached the session (late line or "now available" line) is marked `delivered`, and a kept row then shows only the file. Tests cover both directions.
|
|
116
|
+
- **This artifact.** Marked the second-pass reviewer `required` and added this section.
|
|
117
|
+
|
|
118
|
+
The re-check of these fixes by the second-pass reviewer is pending.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Evidence pointers
|
|
123
|
+
|
|
124
|
+
- `tests/unit/app-voice-transcript-event.test.ts`
|
|
125
|
+
- `tests/unit/app-voice-transcription.test.ts` (deadline line updated)
|
|
126
|
+
- `tests/unit/PostUpdateMigrator-appMedia.test.ts` (voice line refresh)
|
|
127
|
+
- `tests/integration/mobile-native-media.test.ts` ("server voice transcripts (§4.5.1)")
|
|
128
|
+
- `tests/e2e/mobile-native-media-alive.test.ts` ("server voice transcripts (§4.5.1) on the production path")
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Class-Closure Declaration
|
|
133
|
+
|
|
134
|
+
No agent-authored-artifact defect is fixed here. For the `unbounded-self-action` class: **guard.** The one self-triggered action this adds is the late-transcript follow-up line (a notify); provider calls are additionally capped at 3 concurrent per process, with queue time counted toward the 10-minute ceiling. It fires at most once per attachment per machine, from a transcription that itself runs at most once per attachment per machine: the `voice_transcriptions` row is claimed before the provider starts and is never reset to allow a retry (a `running` row left by a crash is read as a failure), the in-process entry arms the follow-up exactly once (`lateArmed`), and an undelivered line is one `transcript_notes` row keyed by attachment id (`INSERT OR IGNORE`) that a delivery acknowledges or the 7-day expiry removes. Steady state under sustained pressure (no live session, provider always slow) is therefore one line per voice note and zero further actions. `defectClass: unbounded-self-action`, `closure: guard`, `guardEvidence: {enforcementType: ratchet, citation: tests/unit/app-voice-transcript-event.test.ts ("two deliveries racing one slow transcription start the provider once and send one follow-up"; "a failure is recorded with its reason and never retried"; "a run a restart interrupted reads as a failure"), howCaught: an unbounded retry or a repeated follow-up would raise the provider call count above 1 or the follow-up count above 1 and fail these assertions}`. The file is not a controller-shape file and `scripts/lint-no-unregistered-self-action.js` reports nothing for it.
|
|
135
|
+
|
|
136
|
+
Reviewer re-check (2026-11-03 03:24 PST): Concur with the review. All four concerns are fixed. Residual note: the new `delivered` column exists only in CREATE TABLE IF NOT EXISTS; a machine that ran an earlier build of this unreleased branch keeps the old table, and its caught write failure can repeat a transcript once. Nothing released has this table.
|