instar 1.3.1611 → 1.3.1613

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.
Files changed (159) hide show
  1. package/dashboard/console/console.css +3 -1
  2. package/dashboard/console/console.js +18 -5
  3. package/dashboard/console/home-model.js +4 -1
  4. package/dashboard/console/messages-model.js +2 -1
  5. package/dashboard/console/messages.js +1 -1
  6. package/dashboard/console/pace-model.js +5 -0
  7. package/dashboard/console/pace.js +3 -1
  8. package/dashboard/console/plans-model.js +17 -1
  9. package/dashboard/console/promises.js +2 -1
  10. package/dashboard/console/secrets-model.js +11 -1
  11. package/dist/commands/server.d.ts.map +1 -1
  12. package/dist/commands/server.js +32 -8
  13. package/dist/commands/server.js.map +1 -1
  14. package/dist/core/AppConversationStore.d.ts +2 -0
  15. package/dist/core/AppConversationStore.d.ts.map +1 -1
  16. package/dist/core/AppConversationStore.js +8 -1
  17. package/dist/core/AppConversationStore.js.map +1 -1
  18. package/dist/core/AppRecordSignatures.d.ts +23 -2
  19. package/dist/core/AppRecordSignatures.d.ts.map +1 -1
  20. package/dist/core/AppRecordSignatures.js +32 -5
  21. package/dist/core/AppRecordSignatures.js.map +1 -1
  22. package/dist/core/LocalAttentionItems.d.ts +49 -0
  23. package/dist/core/LocalAttentionItems.d.ts.map +1 -0
  24. package/dist/core/LocalAttentionItems.js +107 -0
  25. package/dist/core/LocalAttentionItems.js.map +1 -0
  26. package/dist/core/MacDesktopHost.d.ts +3 -0
  27. package/dist/core/MacDesktopHost.d.ts.map +1 -1
  28. package/dist/core/MacDesktopHost.js +10 -0
  29. package/dist/core/MacDesktopHost.js.map +1 -1
  30. package/dist/core/MachineLockStateReader.d.ts.map +1 -1
  31. package/dist/core/MachineLockStateReader.js +6 -1
  32. package/dist/core/MachineLockStateReader.js.map +1 -1
  33. package/dist/core/PostUpdateMigrator.d.ts +6 -0
  34. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  35. package/dist/core/PostUpdateMigrator.js +59 -2
  36. package/dist/core/PostUpdateMigrator.js.map +1 -1
  37. package/dist/core/types.d.ts +12 -0
  38. package/dist/core/types.d.ts.map +1 -1
  39. package/dist/core/types.js.map +1 -1
  40. package/dist/data/standards-guard-index.json +1 -1
  41. package/dist/data/standards-guard-index.meta.json +2 -2
  42. package/dist/data/standards-registry.meta.json +1 -1
  43. package/dist/lifeline/TelegramLifeline.d.ts +2 -0
  44. package/dist/lifeline/TelegramLifeline.d.ts.map +1 -1
  45. package/dist/lifeline/TelegramLifeline.js +28 -0
  46. package/dist/lifeline/TelegramLifeline.js.map +1 -1
  47. package/dist/localVerify/localVerifyWiring.d.ts +11 -0
  48. package/dist/localVerify/localVerifyWiring.d.ts.map +1 -1
  49. package/dist/localVerify/localVerifyWiring.js +9 -0
  50. package/dist/localVerify/localVerifyWiring.js.map +1 -1
  51. package/dist/messaging/TelegramAdapter.d.ts +8 -0
  52. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  53. package/dist/messaging/TelegramAdapter.js +46 -31
  54. package/dist/messaging/TelegramAdapter.js.map +1 -1
  55. package/dist/messaging/shared/MessageLogger.d.ts +2 -0
  56. package/dist/messaging/shared/MessageLogger.d.ts.map +1 -1
  57. package/dist/messaging/shared/MessageLogger.js.map +1 -1
  58. package/dist/messaging/slack/SlackAdapter.d.ts +19 -0
  59. package/dist/messaging/slack/SlackAdapter.d.ts.map +1 -1
  60. package/dist/messaging/slack/SlackAdapter.js +21 -0
  61. package/dist/messaging/slack/SlackAdapter.js.map +1 -1
  62. package/dist/messaging/telegram-origin/OriginForwardToHolder.d.ts +13 -0
  63. package/dist/messaging/telegram-origin/OriginForwardToHolder.d.ts.map +1 -1
  64. package/dist/messaging/telegram-origin/OriginForwardToHolder.js +16 -0
  65. package/dist/messaging/telegram-origin/OriginForwardToHolder.js.map +1 -1
  66. package/dist/messaging/telegram-origin/OriginLegacy.d.ts +8 -0
  67. package/dist/messaging/telegram-origin/OriginLegacy.d.ts.map +1 -1
  68. package/dist/messaging/telegram-origin/OriginLegacy.js +34 -6
  69. package/dist/messaging/telegram-origin/OriginLegacy.js.map +1 -1
  70. package/dist/messaging/telegram-origin/TelegramOriginBoot.d.ts +3 -0
  71. package/dist/messaging/telegram-origin/TelegramOriginBoot.d.ts.map +1 -1
  72. package/dist/messaging/telegram-origin/TelegramOriginBoot.js +54 -10
  73. package/dist/messaging/telegram-origin/TelegramOriginBoot.js.map +1 -1
  74. package/dist/messaging/telegram-origin/types.d.ts +11 -0
  75. package/dist/messaging/telegram-origin/types.d.ts.map +1 -1
  76. package/dist/messaging/telegram-origin/types.js +15 -0
  77. package/dist/messaging/telegram-origin/types.js.map +1 -1
  78. package/dist/monitoring/GreenPrAutoMerger.d.ts.map +1 -1
  79. package/dist/monitoring/GreenPrAutoMerger.js +13 -0
  80. package/dist/monitoring/GreenPrAutoMerger.js.map +1 -1
  81. package/dist/monitoring/greenPrAutomergeWiring.d.ts.map +1 -1
  82. package/dist/monitoring/greenPrAutomergeWiring.js +3 -0
  83. package/dist/monitoring/greenPrAutomergeWiring.js.map +1 -1
  84. package/dist/monitoring/promptRelaySettle.d.ts +35 -0
  85. package/dist/monitoring/promptRelaySettle.d.ts.map +1 -0
  86. package/dist/monitoring/promptRelaySettle.js +58 -0
  87. package/dist/monitoring/promptRelaySettle.js.map +1 -0
  88. package/dist/native/instar-desktop-host +0 -0
  89. package/dist/scaffold/templates.d.ts.map +1 -1
  90. package/dist/scaffold/templates.js +3 -1
  91. package/dist/scaffold/templates.js.map +1 -1
  92. package/dist/server/AgentServer.d.ts +15 -0
  93. package/dist/server/AgentServer.d.ts.map +1 -1
  94. package/dist/server/AgentServer.js +78 -20
  95. package/dist/server/AgentServer.js.map +1 -1
  96. package/dist/server/CapabilityIndex.js +2 -2
  97. package/dist/server/CapabilityIndex.js.map +1 -1
  98. package/dist/server/mobileConversationRoutes.d.ts +16 -3
  99. package/dist/server/mobileConversationRoutes.d.ts.map +1 -1
  100. package/dist/server/mobileConversationRoutes.js +36 -8
  101. package/dist/server/mobileConversationRoutes.js.map +1 -1
  102. package/dist/server/mobilePairingRoutes.d.ts +1 -1
  103. package/dist/server/mobilePairingRoutes.d.ts.map +1 -1
  104. package/dist/server/mobilePairingRoutes.js +5 -2
  105. package/dist/server/mobilePairingRoutes.js.map +1 -1
  106. package/dist/server/mobileSlackRoutes.d.ts +162 -0
  107. package/dist/server/mobileSlackRoutes.d.ts.map +1 -0
  108. package/dist/server/mobileSlackRoutes.js +552 -0
  109. package/dist/server/mobileSlackRoutes.js.map +1 -0
  110. package/dist/server/routes.d.ts +2 -0
  111. package/dist/server/routes.d.ts.map +1 -1
  112. package/dist/server/routes.js +85 -6
  113. package/dist/server/routes.js.map +1 -1
  114. package/package.json +1 -1
  115. package/scripts/build-locked-proof.mjs +125 -0
  116. package/scripts/lib/locked-proof-build.mjs +71 -0
  117. package/scripts/locked-proof/configure-proof-agent.mjs +68 -0
  118. package/scripts/locked-proof/create-proof-user.sh +89 -0
  119. package/scripts/locked-proof/driver.mjs +457 -0
  120. package/scripts/locked-proof/lib.mjs +267 -0
  121. package/scripts/locked-proof/pty-feed.py +59 -0
  122. package/scripts/locked-proof/remove-proof-user.sh +18 -0
  123. package/scripts/locked-proof/reset-attempts.py +52 -0
  124. package/scripts/locked-proof/stage-proof-agents.sh +68 -0
  125. package/src/data/builtin-manifest.json +65 -65
  126. package/src/data/standards-guard-index.json +1 -1
  127. package/src/data/standards-guard-index.meta.json +2 -2
  128. package/src/data/standards-registry.meta.json +1 -1
  129. package/src/scaffold/templates.ts +3 -1
  130. package/upgrades/1.3.1612.md +72 -0
  131. package/upgrades/1.3.1613.md +108 -0
  132. package/upgrades/side-effects/blocker-text-is-the-message.eli16.md +5 -0
  133. package/upgrades/side-effects/blocker-text-is-the-message.md +38 -0
  134. package/upgrades/side-effects/console-needs-row-tidy.eli16.md +13 -0
  135. package/upgrades/side-effects/console-needs-row-tidy.md +24 -0
  136. package/upgrades/side-effects/console-partial-survives-reload.eli16.md +5 -0
  137. package/upgrades/side-effects/console-partial-survives-reload.md +34 -0
  138. package/upgrades/side-effects/console-secret-drop-names.eli16.md +5 -0
  139. package/upgrades/side-effects/console-secret-drop-names.md +34 -0
  140. package/upgrades/side-effects/desktop-tests-stop-touching-tcc.eli16.md +13 -0
  141. package/upgrades/side-effects/desktop-tests-stop-touching-tcc.md +65 -0
  142. package/upgrades/side-effects/green-pr-eligible-needs-passing-record.eli16.md +7 -0
  143. package/upgrades/side-effects/green-pr-eligible-needs-passing-record.md +39 -0
  144. package/upgrades/side-effects/green-pr-poke-unknown-mergeable.eli16.md +5 -0
  145. package/upgrades/side-effects/green-pr-poke-unknown-mergeable.md +41 -0
  146. package/upgrades/side-effects/held-expiry-one-row-per-topic.eli16.md +5 -0
  147. package/upgrades/side-effects/held-expiry-one-row-per-topic.md +37 -0
  148. package/upgrades/side-effects/locked-proof-driver.md +72 -0
  149. package/upgrades/side-effects/locked-proof-test-build.md +64 -0
  150. package/upgrades/side-effects/locked-security-item-no-telegram.md +78 -0
  151. package/upgrades/side-effects/mobile-native-conversations-inc4.md +115 -0
  152. package/upgrades/side-effects/origin-config-unknown-not-missing.eli16.md +16 -0
  153. package/upgrades/side-effects/origin-config-unknown-not-missing.md +38 -0
  154. package/upgrades/side-effects/pool-rows-use-local-machine-name.eli16.md +5 -0
  155. package/upgrades/side-effects/pool-rows-use-local-machine-name.md +34 -0
  156. package/upgrades/side-effects/prompt-gate-settle-before-relay.eli16.md +11 -0
  157. package/upgrades/side-effects/prompt-gate-settle-before-relay.md +57 -0
  158. package/upgrades/side-effects/topic-names-through-lifeline.eli16.md +9 -0
  159. package/upgrades/side-effects/topic-names-through-lifeline.md +39 -0
@@ -0,0 +1,72 @@
1
+ # Side-Effects Review — locked-Mac proof driver and test-user setup scripts
2
+
3
+ **Version / slug:** `locked-proof-driver`
4
+ **Date:** `2026-11-02`
5
+ **Author:** `Echo`
6
+ **Tier:** 1 (maintainer scripts, no runtime source change, nothing runs without `--execute`). Size: 7 new files under `scripts/locked-proof/` (~750 lines), 1 test file. Effort: no spec rounds; first commit attempt.
7
+
8
+ ## Summary of the change
9
+
10
+ The locked-Mac proof plan (`.instar/lanes/locked-mac-proof-plan-134088.md`, items M3 and M5) needs a driver for P12/P15 and repeatable throwaway-user setup. This adds them as scripts only. One change from the plan, per the brief for this work: the agent types the throwaway user's password through the remote-desktop viewer, instead of the operator typing it about 30 times. The password is generated by the setup script, never chosen by a person, kept in the agent vault (`locked_proof_user_password`, stored through `credential-store.mjs` with the value on stdin, Bitwarden first), and read once into the driver's memory. It is not the owner's password: the spec's "no agent ever handles it" concerns the owner's real password, which this run never touches (the test account is empty and throwaway). The user is created without a forced password change at first login (`pwpolicy -clearaccountpolicies`; the plan's `newPasswordRequired` step is dropped).
11
+
12
+ Files: `lib.mjs` (cases, expected table, runner, verdict, password generator, attempt reset rule, target check), `driver.mjs` (real target: sudo against the proof user, the proof agents' HTTP API, the real viewer page through playwright-core with system Chrome), `create-proof-user.sh`, `pty-feed.py`, `stage-proof-agents.sh`, `configure-proof-agent.mjs`, `reset-attempts.py`, `remove-proof-user.sh`.
13
+
14
+ ## Decision-point inventory
15
+
16
+ None in runtime code. The driver's verdict is a test-harness judgement recorded with its evidence; it gates nothing by itself (a passing record still goes through a normal PR to the proven-versions table).
17
+
18
+ ## Review follow-ups (independent review on #2598)
19
+
20
+ 1. Signals are owner-checked: `serverPid` asks `lsof -a -u instarproof` (only the proof user's listener), and `kill()` reads the pid's owner (`ps -o user=`) immediately before every signal and throws for anyone but `instarproof`. `assertProofTarget` now gets the real owner of each agent's listening process and of each agent directory, not a constant.
21
+ 2. `runCase` runs the case in try/finally: whatever throws, `target.recover()` resumes every pid the run SIGSTOPped, clears the guard-stopper and locks the proof session; a recovery problem or the error fails the case by name. `close()` runs the same recovery.
22
+ 3. `configure-proof-agent.mjs` and `reset-attempts.py` refuse without `--execute`, and refuse anything that does not resolve inside `/Users/instarproof` (the reset script's only other accepted root is a test root inside the system temporary directory named by `LOCKED_PROOF_TEST_ROOT`).
23
+ 4. Expectations without a measurement now fail as "not measured" instead of passing silently. Measured from the run's own samples: relock time after a transport drop (15.5/15.6, must be at least the 30 s resume window) and whether the password field is left dirty at the end (15.9, from the store's `dirty` flag, which blocks any later Enter). Lock calls against another session (12.12/12.13) have no observable source yet, so those two cases fail as "not measured" until one exists.
24
+ 5. Bitwarden: the agent's `credential-store.mjs` put the base64 item (which holds the password) on `bw`'s command line. Echo's copy on this machine now pipes it on stdin (`bw create item` / `bw edit item <id>` read the encoded JSON from stdin, per `bw --help`, version 2026.7.0). `create-proof-user.sh` refuses to run if the helper it would call still puts the item on the command line, so the Laptop's copy must be updated before setup. The password is on no command line in this flow: stdin to the helper, stdin to `bw`, the pty prompt to `sysadminctl` and `dscl`.
25
+
26
+ ## 1. Over-block
27
+
28
+ None: no runtime path changes.
29
+
30
+ ## 2. Under-block
31
+
32
+ - The verdict can only judge what it observes. "Lock call against another session" (12.12/12.13) is not directly observable from outside the helper; the driver records the marker outcome and leaves that sub-check to the helper's own audit rows, read at the sitting.
33
+ - The loginwindow submission count is recorded as raw evidence (`loginwindowLines`); its log pattern is unverified until the sitting, so the verdict relies on the helper's `enterPendingAt` records for "no submission after close".
34
+ - Timing of "immediately before Enter" depends on calibration; the plan's rerun rule (offset off by more than 500 ms) is applied by the person running the sitting from the recorded timestamps.
35
+
36
+ ## 3. Level-of-abstraction fit
37
+
38
+ Maintainer tooling outside `src/`, driving the same surfaces an owner uses (viewer page, revoke route, PIN route). It adds no test-only hooks to the helper, so the helper under proof is byte-identical to the shipped one.
39
+
40
+ ## 4. Signal vs authority compliance
41
+
42
+ Not applicable: no runtime authority. The scripts refuse to act on anything but the `instarproof` user running a `-lockedproof` build.
43
+
44
+ ## 5. Interactions
45
+
46
+ - Uses `sudo -n` (passwordless sudo on the proof Mac) to signal only the proof user's processes and read its relock store. `reset-attempts.py` takes the helper's own `state.lock` and refuses while a duty is open.
47
+ - `create-proof-user.sh` depends on `sysadminctl -password -` prompting on its terminal; if a macOS version does not prompt, `pty-feed.py` fails loudly ("never asked") after storing the vault value, and no user exists. Rerun with `--replace-vault`.
48
+ - Starting the proof servers in the proof user's session needs that session to exist (the person's first login).
49
+
50
+ ## 6. External surfaces
51
+
52
+ None for users or agents. A Bitwarden item "Instar locked-proof test user (<host>)" is created when setup runs.
53
+
54
+ ## 7. Multi-machine posture
55
+
56
+ Machine-local by design: the proof runs on one Mac, against a user on that Mac.
57
+
58
+ ## 8. Rollback cost
59
+
60
+ Delete the scripts. If setup was run, `remove-proof-user.sh --execute` deletes the user; the vault key is deleted by the agent.
61
+
62
+ ## Conclusion
63
+
64
+ Adds the missing driver and setup tooling, with the agent typing the throwaway password so the operator's part is two permission grants and a few local actions. Tier 1.
65
+
66
+ ## Second-pass review (if required)
67
+
68
+ Not required: no runtime gate, session lifecycle or messaging change.
69
+
70
+ ## Evidence pointers
71
+
72
+ - `tests/unit/locked-proof-driver.test.ts`
@@ -0,0 +1,64 @@
1
+ # Side-Effects Review — locked-screen test build script
2
+
3
+ **Version / slug:** `locked-proof-test-build`
4
+ **Date:** `2026-11-02`
5
+ **Author:** `Echo`
6
+ **Tier:** 1 (maintainer script + test, no runtime source change). Size: 2 new scripts (~170 lines), 1 new test file. Effort: no spec rounds, first commit attempt.
7
+
8
+ ## Summary of the change
9
+
10
+ The locked-screen spec says P12 and P15 run "on a test build" that "alone compiles in a test proven-versions table; no environment variable, config key or route can reach it in dist". That build did not exist. This adds:
11
+ 1. `scripts/lib/locked-proof-build.mjs`: pure helpers (version naming, macOS major parsing, the table rewrite, the committed-table proof check).
12
+ 2. `scripts/build-locked-proof.mjs`: `git archive` of a committed revision into a scratch directory outside the checkout, rewrite of the scratch copy's `src/core/DesktopProvenVersions.ts` (one `PROOF-RUN-IN-PROGRESS <version>` row), version `<release>-lockedproof.<n>`, `npm run build`, `npm pack --ignore-scripts`, manifest with commit, macOS major, helper `sourceHash`, tgz SHA-256.
13
+ 3. `tests/unit/locked-proof-build.test.ts`, including a guard that every committed table row cites existing proof records for P12 and P15 and never the marker.
14
+
15
+ ## Decision-point inventory
16
+
17
+ - The macOS-version admission decision (`provenVersion`) is unchanged. The test build only feeds it a different table in a scratch copy, which is the mechanism the spec names.
18
+
19
+ ## 1. Over-block
20
+
21
+ None: no runtime path changes.
22
+
23
+ ## 2. Under-block
24
+
25
+ - Someone could copy the marker row into the committed table by hand; the new unit test fails the suite in that case.
26
+ - The test package could be installed somewhere other than the throwaway user. It is never published to npm and carries a distinct version; installation is a deliberate act by the proof setup scripts. The proof agent must run with auto-update off, or the updater would replace it with the latest release (handled by the setup scripts in the next PR).
27
+
28
+ ## 3. Level-of-abstraction fit
29
+
30
+ A build-time source rewrite is exactly what the spec asks for (no env/config/route reaches the table). Kept as a maintainer script, not shipped runtime code.
31
+
32
+ ## 4. Signal vs authority compliance
33
+
34
+ No decision logic added. The unit guard is a CI check on committed data.
35
+
36
+ ## 5. Interactions
37
+
38
+ - `npm run build` in the scratch copy runs the normal build chain with `INSTAR_RELEASE_PRIVATE_KEY_PEM`, `INSTAR_RELEASE_PRIVATE_KEY_PEM_PATH` and `INSTAR_RELEASE_BUILD` removed from its environment, so the lock-file signer writes an unsigned lock-file (the scratch tree, a fresh `git archive`, has no `.instar-release-keys/`). A test build is never release-signed.
39
+ - The scratch copy symlinks this checkout's `node_modules` for the build and removes the link before packing.
40
+ - `tests/e2e/desktop-locked-lifecycle.test.ts` still asserts the shipped table is empty; unchanged.
41
+
42
+ ## 6. External surfaces
43
+
44
+ None for users or agents. `scripts/` ships in the npm package, so the script is present in installs but does nothing unless run from a git checkout.
45
+
46
+ ## 7. Multi-machine posture
47
+
48
+ Machine-local by design: a build tool run on one Mac for that Mac's macOS major.
49
+
50
+ ## 8. Rollback cost
51
+
52
+ Delete the scripts and test. Nothing persists.
53
+
54
+ ## Conclusion
55
+
56
+ Adds the spec's missing test build, plus a guard that the committed table can only hold proven rows. Tier 1.
57
+
58
+ ## Second-pass review (if required)
59
+
60
+ Not required: no gate, session lifecycle or messaging change.
61
+
62
+ ## Evidence pointers
63
+
64
+ - `tests/unit/locked-proof-build.test.ts`
@@ -0,0 +1,78 @@
1
+ # Side-Effects Review — locked-screen security item reaches an attention list without Telegram
2
+
3
+ **Version / slug:** `locked-security-item-no-telegram`
4
+ **Date:** `2026-11-02`
5
+ **Author:** `Echo`
6
+ **Tier:** 1 (small, reversible: a revert restores the previous behaviour exactly). Size: 5 src files changed + 1 new (about 150 lines), 3 new test files. Effort: no spec rounds (Tier 1), first commit attempt.
7
+
8
+ ## Summary of the change
9
+
10
+ `AgentServer`'s `raiseSecurity` dependency for `DesktopLockedController` went only through `telegramAdapter.createAttentionItem`. With no Telegram adapter it logged a warning and returned `false`, so on an agent without Telegram the owner was never told that a Mac may still be unlocked (relock failed), that the relock record was unreadable, or that locked-screen access was turned off. The locked-Mac proof plan (topic 134088, P12 cases 12.8/12.11) needs this item to be raised within about 10 s on a test agent with no Telegram, and it is a real gap for any agent run without Telegram.
11
+
12
+ Changes:
13
+ 1. New `src/core/LocalAttentionItems.ts`: a file-backed list (`state/local-attention-items.json`, 0600, atomic tmp+rename write), one row per id, a re-raise refreshes and reopens the row, bounded to 200 rows dropping the oldest closed rows first and never an open one. `raise()` returns `true` only when the row is on disk.
14
+ 2. `AgentServer`: the `raiseSecurity` closure becomes a private method `raiseLockedSecurityItem`. With Telegram: unchanged. Without: stores the item locally (HIGH, category security, source agent) and calls `mobilePairing.push.onHubPost` (best effort); returns `false` with a degradation report when the write fails. The store is constructed only when there is no Telegram adapter, and passed to the route context.
15
+ 3. `routes.ts`: `GET /attention`, `GET /attention/:id`, `PATCH /attention/:id` serve the local list when `ctx.telegram` is null and `ctx.localAttention` is set. Other attention routes (POST, lifecycle, clear, remote-ack) keep their 503 without Telegram.
16
+ 4. `mobilePairingRoutes.ts`: the notification title lookup `GET /mobile/attention/:id` falls back to the local list.
17
+ 5. CLAUDE.md template + `migrateClaudeMd` bullet (sniff `local-attention-items.json`).
18
+
19
+ ## Decision-point inventory
20
+
21
+ - `DesktopLockedController` "told" decision (marks the relock record `reported`) — unchanged logic; its input (`raiseSecurity` result) is now honest on agents without Telegram (true only when stored).
22
+ - No new gate, filter or block.
23
+
24
+ ## 1. Over-block
25
+
26
+ None. Nothing is refused that was accepted before. Routes that answered 503 now answer 200 only when a local list exists (no Telegram).
27
+
28
+ ## 2. Under-block
29
+
30
+ - Without Telegram, `POST /attention` still answers 503: an agent cannot queue its own items there. This change is about the owner seeing security items; the agent itself writing to the list is a separate question with no current user.
31
+ - The phone push is best-effort; if APNs is not configured the item is still on the list and in the app.
32
+ - The local list is not merged into `GET /attention?scope=pool` from peers that have Telegram (a peer's pool read calls this machine's `/attention`, which now answers with the local list, so it IS visible to Telegram peers' pool view).
33
+
34
+ ## 3. Level-of-abstraction fit
35
+
36
+ The attention store living inside `TelegramAdapter` is the root cause; moving it out is a large refactor. This adds the smallest store at the same route surface, so readers (`GET /attention`, the app) need no new address.
37
+
38
+ ## 4. Signal vs authority compliance
39
+
40
+ Signal only: the store records and reports. No blocking authority added.
41
+
42
+ ## 5. Interactions
43
+
44
+ - `DesktopLockedController.sweep` retries an unreported `relock-failed` each sweep until `raiseSecurity` returns true; now it gets true on a no-Telegram agent after the first successful write, so `mark-reported` runs and the guard's record closes as before with Telegram.
45
+ - `disable()` raises an item each time locked access is turned off; the row is reused (same id), so repeated disables refresh one row.
46
+ - Mobile push dedupe key includes a timestamp, so a re-raise can push again; push budgets (2-minute fold, daily cap) still bound it.
47
+ - No change when a Telegram adapter exists: `localAttention` is null and every route takes its old path.
48
+
49
+ ## 6. External surfaces
50
+
51
+ `GET /attention` without Telegram changes from 503 to 200 with `surface: "local"`. Any client treating 503 as "no Telegram" now gets a list instead; the dashboard and the app already render an item list. CLAUDE.md gains one bullet.
52
+
53
+ ## 7. Multi-machine posture
54
+
55
+ Machine-local by design: the security item concerns this Mac. Peers see it through their existing `GET /attention?scope=pool` fan-out, which calls this machine's `/attention`.
56
+
57
+ ## 8. Rollback cost
58
+
59
+ Revert the PR. The leftover `state/local-attention-items.json` is inert. No migration.
60
+
61
+ ## Class-Closure Declaration
62
+
63
+ - `unbounded-self-action`: closure `n/a`. No new self-triggered loop. The added local write and phone push run once per raise from the existing DesktopLockedController sweep, which stops raising a condition once `raiseSecurity` returns true (`mark-reported`); a failed write retries at the existing 10 s sweep cadence only while the condition is open. Each push passes the MobilePush per-device budgets and folding.
64
+
65
+ ## Conclusion
66
+
67
+ Closes a real gap where a security warning was dropped on agents without Telegram, with no change for agents that have Telegram. Tier 1.
68
+
69
+ ## Second-pass review (if required)
70
+
71
+ Not required: no block/allow, session lifecycle, gate or sentinel logic changes; this only gives an existing report a place to land.
72
+
73
+ ## Evidence pointers
74
+
75
+ - `tests/unit/local-attention-items.test.ts`
76
+ - `tests/integration/attention-local-without-telegram.test.ts`
77
+ - `tests/e2e/locked-security-item-without-telegram.test.ts`
78
+ - `tests/unit/PostUpdateMigrator-attentionWithoutTelegram.test.ts`
@@ -0,0 +1,115 @@
1
+ # Side-Effects Review — Native app conversations, increment 4 (Slack conversations from the app)
2
+
3
+ **Version / slug:** `mobile-native-conversations-inc4`
4
+ **Date:** `2026-11-02`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not run (--second-pass false, as instructed for this build)`
7
+ **Tier:** 2 (spec-driven; inbound dispatch and a signed-record format). Size: 11 source files (~520 added source lines, one new route module) + 3 new test files (~560 lines). No spec review round (the spec was already converged and approved); commit attempts recorded in the PR.
8
+
9
+ ## Summary of the change
10
+
11
+ Increment 4 of docs/specs/mobile-native-conversations.md (§4.9 Slack, §4.10 allowlist), behind the existing boot-read `mobilePairing.native.enabled` gate.
12
+
13
+ - **List:** `GET /mobile/conversations` adds this machine's local Slack conversations (registry entries whose origin is not `replicated`): `{id: <negative durable id>, platform: "slack", title: label or "#<channel>" (+" (thread)"), lastActivity, lastPreview}`, merged on the same lossless cursor as app and Telegram rows (sort key `slack:<id>`). `?scope=pool` accepts peers' Slack rows (re-validated: negative safe integer). With `mobilePairing.slackOwnerUserId` unset the rows are withheld and the response says `slackOwnerUnset: true`.
14
+ - **History:** `GET /mobile/chat/slack/:id/messages` (device, or the agent's Bearer token) merges the tail of `slack-messages.jsonl` (newest 5000 lines within 8 MiB), the adapter's in-memory ring, and the app-sent messages kept in the app conversation store under the registry's canonical key. App-sent rows carry `originVerified` (and their delivery state); rows typed in Slack and agent replies read `none`. Pages backwards by `(at, eventId)`.
15
+ - **Send:** `POST /mobile/chat/slack/:id` follows §4.2: authenticate → look up (`MobileChatRequestStore`) → emergency stop skips the setting and rate limit → preflight (chat setting, 20/min, request-id age, `scanInbound`) → serving-lease check → token removal, credential signal → reserve → persist (fsync) with the signed origin and a **v2 receipt** → a copy in `slack-messages.jsonl` carrying the store `eventId` → stop/pause intercepts → the server's real Slack inbound dispatch with the operator as sender (`mobilePairing.slackOwnerUserId`, `threadline.ownerName`) and `appOrigin` → delivery event → mirror "📱 <owner> (from the app): <text with credential spans hidden>" → done. Boot recovery of an interrupted send is the existing `recoverStranded` (`op: send` with the canonical key).
16
+ - **v2 receipt:** a Slack send path names a machine-local alias, so the receipt also signs the canonical key (`instar-app-receipt-v2`, fields `v: 2`, `conversationKey`). `verifyRecordedOrigin` requires, for a `slack:` record, a POST to `/mobile/chat/slack/-N` and a v2 receipt naming the record's key; a v1 receipt, a different key, or a non-Slack path reads `failed`. Non-Slack records are verified exactly as before (a v2 receipt on them must also name their key).
17
+ - **Slack dispatch outcome:** `slackInboundDispatch` now returns `delivered | queued | refused` (previously `void`); the live and forwarded Slack paths ignore it. A message sent from the app gets ` (📱 signed)` (verified) or ` (📱 from the app)` appended to its tag by the server, after the sanitized name.
18
+ - **Agent replies in the Slack log:** `handleSlackReply` now writes the agent's reply to `slack-messages.jsonl` (Slack never echoes the bot's messages), so the app's Slack history shows both sides. Inbound Slack rows gain `threadTs` when they sit in a thread.
19
+ - **Config:** `mobilePairing.slackOwnerUserId` (optional, `U…`/`W…`). Never inferred.
20
+ - **Awareness:** CLAUDE.md section "Slack conversations from the Instar app" (template + `migrateClaudeMd`, sniffed by heading); CapabilityIndex lists the two routes.
21
+
22
+ ## Decision-point inventory
23
+
24
+ - Slack send preflight — add — invariant: the same content-free checks as the app/Telegram send routes; only exact authority tokens are removed; credential-shaped text is a signal.
25
+ - Owner identity — add — invariant: a config field (`slackOwnerUserId`) that must also be an authorized user on the Slack adapter; never inferred from content.
26
+ - Serving-lease check — add — invariant (as the Telegram chat route): refused before anything is saved.
27
+ - Origin verdict for Slack records — modify — invariant cryptographic checks (v2 receipt).
28
+
29
+ ---
30
+
31
+ ## 1. Over-block
32
+
33
+ - A machine that does not hold the serving lease answers `409 not-serving` before saving, as the Telegram route does; with the session pool on, a Slack conversation owned by another machine is still sent through this machine's dispatch only if this machine holds the lease (the dispatch then re-checks ownership through `SpawnAdmission`). No message is lost: nothing is saved on a refusal and the app keeps the text.
34
+ - `slackOwnerUserId` unset or not an authorized Slack user ⇒ 503 with a plain reason. Intentional (spec: never inferred).
35
+ - A thread conversation's history shows only rows that say they are in that thread; Slack log rows written before this change have no thread field, so older thread replies are not shown in a thread view (they still appear nowhere else either; Slack itself keeps them).
36
+
37
+ ---
38
+
39
+ ## 2. Under-block
40
+
41
+ - History reads a bounded tail of `slack-messages.jsonl`; messages older than the newest 5000 lines are not reachable from the app (app-sent messages are, from the store, never rotated). Slack keeps its own full history.
42
+ - **Slack ids are local to one machine (review fix).** Every Slack list row names the machine that serves it (`machine`), a pool list never includes another machine's Slack rows (a peer row with the same number would otherwise collapse onto this machine's row, or name a different conversation), and both Slack routes require the row's machine (`?machine=` on the read, `machine` in the signed send body): another machine answers 409 `wrong-machine` (with `servedBy`), none answers 400 `machine-required`, before anything is read, saved or sent. A Slack conversation is therefore only reachable through the machine that lists it; with several machines that is the one the phone is paired to.
43
+ - Verification of a Slack record is relative to the canonical key: a workspace id that was `_` on one machine and `T…` on another yields two keys (two store files) for one channel; each record still verifies under its own key.
44
+
45
+ ---
46
+
47
+ ## 3. Level-of-abstraction fit
48
+
49
+ Reuses the app conversation service (request store, store, verify, delivery events, boot recovery), `buildSignedOrigin`, the registry's canonical key, and the server's own Slack inbound dispatch (the same function the live and mesh-forwarded Slack paths run), so a message from the app gets the same session lookup, spawn admission, context and relay instructions as a Slack message. The bridge is a thin adapter over SlackAdapter with an interface small enough to stub in tests.
50
+
51
+ ---
52
+
53
+ ## 4. Signal vs authority compliance
54
+
55
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
56
+
57
+ - [ ] No — this change produces a signal consumed by an existing smart gate.
58
+ - [x] No — this change has no brittle block/allow surface.
59
+ - [ ] Yes — but the logic is a smart gate with full conversational context.
60
+ - [ ] ⚠️ Yes, with brittle logic — STOP.
61
+
62
+ No content check refuses a message. The added refusals are structural (feature setting, rate, request-id age, lease, configured owner identity); credential-shaped text only adds `credentialShaped: true` and is hidden in the mirror copy, never refused.
63
+
64
+ ---
65
+
66
+ ## 4b. Judgment-point check (Judgment Within Floors standard)
67
+
68
+ No new judgment point. Credential-shape detection is the existing `detectOutboundCredential` signal (the spec's decision table classifies it as a judgment candidate whose floor never refuses).
69
+
70
+ ---
71
+
72
+ ## 5. Interactions
73
+
74
+ - **Slack log readers:** PresenceProxy's Slack log check and the Slack compaction-resume path now also see the agent's own replies in `slack-messages.jsonl`. Both look for "did the agent answer after the user's last message"; seeing the reply is the correct answer (previously they only learned it through `onMessageLogged`). `slack-cli` search shows the replies too.
75
+ - **slackInboundDispatch** return type change: callers `await`/`void` it and ignore the value; tsc and the Slack wiring tests pass.
76
+ - **Tag:** the app mark is appended inside the bracket after `(uid:U…)`; the existing tag parser in sessions only reads `[slack:<channel>`.
77
+ - **Mirror loop:** the mirror is posted by the bot; Slack does not deliver bot messages back as inbound, so it cannot re-enter dispatch.
78
+ - **Emergency stop:** an app stop into Slack kills every running session, exactly what a Slack "emergency stop" does.
79
+
80
+ ---
81
+
82
+ ## 6. External surfaces
83
+
84
+ Two new device routes (allowlisted for active devices: `POST /mobile/chat/slack/-N`, `GET /mobile/chat/slack/-N/messages`, both requiring the list row's `machine`), Slack rows in the conversation list, a new optional config field, a new receipt version in stored records (replicated; an older machine reading a v2 Slack record cannot verify it and reads `failed` for a Slack path — such records only exist once this version runs), a new message posted into Slack (the mirror) on each app send, and agent replies added to the Slack log. CLAUDE.md gains a section on update.
85
+
86
+ - **iOS app:** must list `platform: "slack"` rows, send to `/mobile/chat/slack/<id>` with the row's `machine` in the body and read `/mobile/chat/slack/<id>/messages?machine=<row machine>`, show `slackOwnerUnset` as a plain hint, and show marks from `originVerified` (increment 5).
87
+ - **Operator surface:** setting the owner id is done by the agent after the operator's yes in conversation (no PIN, no file editing by the operator).
88
+
89
+ ---
90
+
91
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
92
+
93
+ - App-sent Slack messages: **replicated** in the app conversation store under the canonical Slack key (the existing `appConversations` replicated kind); the v2 receipt makes them checkable on any machine.
94
+ - Slack rows in the list and Slack history: **machine-local BY DESIGN** (a Slack conversation id is minted per machine): the list carries only this machine's Slack rows even with `?scope=pool`, and the routes refuse any other machine's id.
95
+ - Sending: on the serving-lease holder (where Slack inbound is dispatched).
96
+
97
+ ---
98
+
99
+ ## 8. Rollback cost
100
+
101
+ Switch `mobilePairing.native.enabled` off: the Slack routes answer 503 and the list has no Slack rows (the conversation service is not built). Unsetting `slackOwnerUserId` hides Slack alone. Reverting the PR leaves stored Slack records with v2 receipts that an older version reads as `failed`/unknown (display only) and extra agent-reply rows in `slack-messages.jsonl` (harmless log rows). No data migration.
102
+
103
+ ---
104
+
105
+ ## Class-Closure Declaration (display-only mirror)
106
+
107
+ Not applicable: no agent-authored-artifact defect. The mirror post fires once per operator app send (an external event), never on a timer or its own output.
108
+
109
+ ---
110
+
111
+ ## Second-pass review
112
+
113
+ Not run for this build (`--second-pass false`, per the build instructions). The PR is the review surface.
114
+
115
+ Review follow-up (Slack ids are per machine): independent reviewer — Concur with the review. The machine check runs before any read or send in both routes; the query is inside the signed path and the body field inside the body hash; the agent read works with ?machine=; pool lists drop peers' Slack rows; the CLAUDE.md text matches (it now names the 400 too).
@@ -0,0 +1,16 @@
1
+ # Why bug-topic messages from Mama PC failed, in plain words
2
+
3
+ Every few seconds, each machine re-reads its own settings file so it knows where to send Telegram messages. Bug topics live in a separate Telegram group, and the settings say which one.
4
+
5
+ Mama PC sometimes gets so busy that its server freezes for 10 to 30 seconds. When that happens, the settings read takes longer than its 2-second limit and is thrown away. The code then behaved as if the settings had no bug group at all, and refused messages to bug topics with "no messaging config issuesChatId". That was false: the setting was there the whole time.
6
+
7
+ Those refused messages were saved for a retry. But the retry step only trusts a small set of "definitely not sent" answers, and this refusal was not one of them. So it marked them "may already be delivered" and never sent them. They were lost without anyone being told.
8
+
9
+ After this change:
10
+ - If the settings can't be read, the refusal says so honestly ("could not read config", with the reason), and it is marked as safe to retry.
11
+ - The retry step recognises this refusal, and the two older ones (the misleading "no bug group" one, and the bare "display authority unavailable" one that hit a message on the Studio today), as "definitely not sent", so the message goes out once the settings read again. It still passes the usual tone check and duplicate check, and it still expires after 6 hours.
12
+ - The server log now says why a settings read failed, so the next time this happens the cause is written down.
13
+
14
+ Nothing changes about the safety rule that a bug-topic message is never sent to the main group by mistake.
15
+
16
+ Why Mama PC freezes is a separate problem and is reported on its own.
@@ -0,0 +1,38 @@
1
+ # Side-effects review: an unreadable config is "unknown", not "missing"
2
+
3
+ Tier 1. 5 runtime/test files plus artifacts, about 120 lines. One review round.
4
+
5
+ ## Change
6
+ 1. `TelegramOriginBoot.ts`: the 5-second config refresh used to end in `catch { snapshot = null }`. The snapshot then fed `toWireParams` with `issuesChatId: undefined`, so a failed read surfaced as `IssuesChatNotConfiguredError` ("this process has no messaging config issuesChatId"). Now, with no snapshot, an issues-topic send throws `OriginConfigUnavailableError(reason)` instead, and the sender-display check throws the same error when the snapshot is stale. The refresh records a bounded, secret-free reason code and logs it once per change (and on recovery). `refresh()` itself is unchanged in what it accepts.
7
+ 2. `types.ts`: new `OriginConfigUnavailableError` (retryable, `reason`). Its message is still exactly `origin-display-authority-unavailable`, so existing string matchers keep working.
8
+ 3. `routes.ts` (`POST /telegram/reply` catch): this error now answers `503 {error:'origin-config-unavailable', reason, retryable:true, message}` instead of a generic 500.
9
+ 4. `OriginLegacy.ts`: `snapshotLegacyOrigin` treats two exact bodies on the latest attempt as definite non-delivery, the same way PR 2596 does for the boot beacon: that 503 body, and the 500 body `{"error":"issues-chat-not-configured: topic N belongs to the issues group…"}`. It also accepts the bare 500 `{"error":"origin-display-authority-unavailable"}` that earlier releases returned when config could not be read; that text is thrown only by display resolution inside `prepareBot`. All of these are thrown while a send is being prepared, before admission and before any network call.
10
+
11
+ ## 1. Over-block
12
+ None added. A send that was refused before is still refused while config is unreadable; only the reason and status change. Main-group protection is unchanged: with no snapshot an issues topic is never translated to the main chat, and `authorize()` still requires a healthy snapshot.
13
+
14
+ ## 2. Under-block
15
+ The underlying stall on a busy machine is not fixed here (see Interactions). Sends during a stall still wait; they are now retried instead of lost. A row with several attempts qualifies only if every earlier attempt was 401/404 (the table keeps only the latest body), which is the safe direction.
16
+
17
+ ## 3. Level-of-abstraction fit
18
+ The misreport was produced where the snapshot is built, so it is fixed there; the HTTP mapping lives in the existing reply-route catch; the replay proof lives in the existing replay check. No new layer.
19
+
20
+ ## 4. Signal vs authority
21
+ The replay check is an authority (it lets a held message be sent). It is deterministic and narrow: exact bodies from our own reply route, exact key sets, a 1 KiB cap, and a body relayed from another machine (`relayedFromHolder`) is rejected (tested). Replayed rows still pass the legacy tone review, content dedup and the original 6-hour deadline.
22
+
23
+ ## 5. Interactions
24
+ - PR 2596 edits the same lines of `snapshotLegacyOrigin` (boot-beacon 503). The two compose: both are "latest attempt carries an exact pre-dispatch body". Whichever lands second rebases into one helper.
25
+ - The relay script already queues any 5xx ("Queued for recovery"), so the new 503 needs no script change.
26
+ - The stalls behind this (server event loop blocked for 10–30 s, then the lifeline restarting it after 60 s unresponsive) are a separate problem and are reported separately.
27
+
28
+ ## 6. External surfaces
29
+ `POST /telegram/reply` returns 503 with a new error code for this case instead of 500. Callers that treat 5xx as retryable (the relay script) behave as before. Messages refused this way are now delivered later instead of silently dropped.
30
+
31
+ ## 7. Multi-machine posture
32
+ Machine-local by design: each process reads its own config and imports its own queue. This matters most on standbys, which send through the lease holder but prepare locally.
33
+
34
+ ## 8. Rollback cost
35
+ Revert the commit. Rows already replayed were delivered once; nothing to repair.
36
+
37
+ ## Second-pass review
38
+ Concur with the review. Both refusals are raised in `prepareBot` (wireParams, then display) before admission. Every failure after dispatch is a `TelegramOriginHoldError`, and the adapter does not fall back to a second send. There is no partial multi-chunk send, because chunks are children of one sealed operation built after translation. A holder's relayed refusal carries `relayedFromHolder`, which fails the exact key check. The relay script stores the body untruncated (the 1024 cap applies only to the event payload), and both bodies are well under 1 KiB; an oversized body fails safe. `OriginConfigUnavailableError` is matched by `instanceof` only in the reply route; every other caller matches the unchanged message. Main-group protection is intact. Logged reasons are fixed codes only. The integration test's wait was lengthened so a loaded machine does not make it flaky.
@@ -0,0 +1,5 @@
1
+ # One machine, one name, everywhere on the dashboard
2
+
3
+ Each machine running the agent keeps its own list of the other machines and what to call them. Those lists can disagree. Today one Mac was called "Mac M5 Pro" in the Studio's list, while that same Mac called itself "Mac Lan". When the dashboard gathered information from every machine, some rows used the name the Studio knows and others used the name the Mac gave itself. So the Machines page said "Mac M5 Pro" while an alert on the home page said "Mac Lan: Remote Desktop needs its Mac permission again", and it looked like two different machines.
4
+
5
+ Now, when the dashboard gathers rows from other machines, it names each machine using the list of the machine you are looking at, and only falls back to the machine's own name if it has none. Alerts whose title starts with the machine's own name get that start replaced with the same name. The result is that a machine has one name everywhere on a given dashboard. Which name is right is still a choice for the operator. A rename is still recorded only on the machine where it is made, so each machine's own dashboard follows its own list; making a rename reach every machine is a separate fix.
@@ -0,0 +1,34 @@
1
+ # Side-effects review: pool reads name each machine from THIS machine's registry
2
+
3
+ ## Change
4
+ `src/server/routes.ts`, three pool merges (`GET /sessions?scope=pool`, `GET /jobs?scope=pool`, `GET /attention?scope=pool`): a peer row's `machineNickname` now comes from this machine's registry first (`machinePoolRegistry.getCapacity(id).nickname`), and the peer's self-reported name is the fallback (it was the other way round). For attention items, a title the peer prefixed with its own name (`"<own>: …"`) is re-prefixed with the registry name when the two differ.
5
+
6
+ ## Evidence
7
+ On the Studio (2026-11-02) `GET /pool` named `m_a8048693…` "Mac M5 Pro" (the Studio's durable registry), while that machine's own attention items arrived as "Mac Lan: Remote Desktop needs its Mac permission again" with `machineNickname: "Mac Lan"`. The dashboard showed the same machine under two names (Machines page vs needs-you).
8
+
9
+ ## 1. Over-block
10
+ None.
11
+
12
+ ## 2. Under-block
13
+ Free text inside a body that names the peer is not rewritten. The registries themselves still disagree across machines (machine registry is not shared on a git-ignored registry); which name is correct is the operator's call. `PATCH /pool/machines/:id` updates only the local registry, so a rename applies to this machine's dashboard only (filed separately).
14
+
15
+ ## 3. Level-of-abstraction fit
16
+ The merge is where rows from several machines are composed for one viewer; naming there keeps every page consistent without per-page fixes.
17
+
18
+ ## 4. Signal vs authority
19
+ No decision logic; display data only. Routing uses `machineId`, unchanged.
20
+
21
+ ## 5. Interactions
22
+ P17 coalescing keys on `sourceContext`, not title or nickname: unchanged. Rows from this machine are unchanged.
23
+
24
+ ## 6. External surfaces
25
+ Pool API rows carry the viewer's name for a machine. A peer with no registry entry here keeps its own name.
26
+
27
+ ## 6b. Operator-surface quality (Operator-Surface Quality standard)
28
+ Each machine has one name across the Machines page, needs-you rows, sessions and schedule, so the operator never wonders whether "Mac Lan" and "Mac M5 Pro" are two machines.
29
+
30
+ ## 7. Multi-machine posture
31
+ Proxied-on-read: each viewing machine names peers from its own registry. Registry convergence across machines is separate.
32
+
33
+ ## 8. Rollback cost
34
+ Revert.
@@ -0,0 +1,11 @@
1
+ # Prompt Gate waits before telling you about a prompt (plain-English overview)
2
+
3
+ When one of my sessions stops at a question on its screen (a yes/no confirmation, a menu), a part of Instar called Prompt Gate copies that question to your Telegram topic so you can answer it from your phone. Until now it did that the instant it saw the question.
4
+
5
+ The trouble is that many of those questions get answered without you. When a session hits its account's usage limit, Instar moves it to another account; when Claude Code asks a low-level permission question, a safety floor answers it; sometimes the session is simply restarted. In each case you still got a message asking you to choose, about something that no longer needed you. On 2 November that happened twice in one minute, plus a separate notice saying "Prompt Gate is now active".
6
+
7
+ What changes: after seeing a question, Prompt Gate now waits 45 seconds and looks at the session's screen again. If the question is still there, you get the message as before. If it's gone, or the session no longer exists, nothing is sent. If the screen can't be read, it sends anyway, so a question that really needs you is never dropped. The "now active" notice becomes one short line on the first real question instead of its own message.
8
+
9
+ What you might notice: fewer of these messages, and a real one arrives about 45 seconds after the question appears. The wait can be changed or switched off with one setting.
10
+
11
+ If the screen check itself fails, that counts as "can't tell", and the message is sent.
@@ -0,0 +1,57 @@
1
+ # Side-Effects Review — Prompt Gate waits before relaying
2
+
3
+ **Version / slug:** `prompt-gate-settle-before-relay`
4
+ **Date:** `2026-11-02`
5
+ **Author:** `Echo`
6
+ **Second-pass reviewer:** `independent reviewer subagent (see below)`
7
+
8
+ ## Summary of the change
9
+
10
+ Operator report 2026-11-02 13:50 (topic 134088, with a photo): "I feel like [you] shouldn't be notifying for prompts that are handled automatically." At 13:42 two prompts from the operator's own session (a confirmation and Claude Code's weekly-usage-limit menu) were relayed, plus the standalone "Prompt Gate is now active" disclosure; both prompts were resolved without him (the session moved to another account).
11
+
12
+ 1. `src/monitoring/promptRelaySettle.ts` (new): `settleBeforeRelay(prompt, deps)` waits `settleMs`, then: session not alive → no relay; prompt anchors (option labels, else the raw text's last lines) found in the last 20 pane lines → relay; not found → no relay; pane unreadable or no anchor → relay.
13
+ 2. `src/commands/server.ts`: the relay branch calls it before Telegram/Slack relay, with `sessionManager.isSessionAlive` and `sessionManager.captureOutput`. A skipped relay is logged and resets the detector for that session.
14
+ 3. `src/core/types.ts`: `promptGate.relaySettleMs` (default 45000; 0 = immediate).
15
+ 4. `src/messaging/TelegramAdapter.ts`: the first-use disclosure is appended to the first relayed prompt as one line instead of a separate message.
16
+
17
+ ## Decision-point inventory
18
+
19
+ - `settleBeforeRelay` — add — invariant: whether to send a notification about a prompt. Deterministic by design: it only suppresses a message when the live pane positively no longer shows the prompt or the session is gone; every uncertain case relays.
20
+
21
+ ## 1. Over-block
22
+
23
+ A prompt answered by the operator at the terminal within 45 s is not relayed (correct). A prompt whose menu text is redrawn differently (labels reworded) would read as cleared and not relay; anchors are the detector's own parsed labels from the same pane, so a redraw of the same menu keeps them. A prompt pushed out of the last 20 lines by later output while still waiting is possible but unlikely for a blocking prompt (the session is stopped on it).
24
+
25
+ ## 2. Under-block
26
+
27
+ A prompt that stays on screen but is being handled by another mechanism slower than 45 s is still relayed. The window is configurable.
28
+
29
+ ## 3. Level-of-abstraction fit
30
+
31
+ The relay decision lives in the one server relay branch that both Telegram and Slack share, so both channels get it. The detector and classifier are unchanged.
32
+
33
+ ## 4. Signal vs authority compliance
34
+
35
+ It never answers or blocks a prompt; it only decides whether to send a message, and fails toward sending. Reference: docs/signal-vs-authority.md.
36
+
37
+ ## 5. Interactions
38
+
39
+ Auto-dismiss and auto-approve paths run before it and are unchanged. Relayed prompts arrive ~45 s later, so a reply relayed back targets a prompt that was confirmed still present moments before. Detector dedup/cooldown is reset on a skipped relay, as it is after a handled one.
40
+
41
+ ## 6. External surfaces
42
+
43
+ Fewer Telegram/Slack messages; a ~45 s delay on genuine prompts; one less standalone message per topic.
44
+
45
+ ## 7. Multi-machine posture
46
+
47
+ Machine-local by design (`hardware-bound-resource`: the prompt is on this machine's tmux pane, and only this machine can read it).
48
+
49
+ ## 8. Rollback cost
50
+
51
+ Set `promptGate.relaySettleMs: 0`, or revert. No state.
52
+
53
+ ## Second-pass review
54
+
55
+ Independent reviewer — Concur with the review. Argument types match (prompt.sessionName is the tmux name, as the auto-dismiss sendKey already uses); a prompt is dropped only when the session is gone or no anchor is in the pane tail, every uncertain case relays; a skipped relay resets the detector like the auto-approve path; the 45 s wait is per prompt and does not hold the event loop; the appended note is fixed text. Minor: the topic is marked disclosed before the send succeeds, unchanged from before.
56
+
57
+ Follow-up: Local Verify flagged the pane-read catch for the silent-fallback rule; it now carries the marker explaining that an unreadable pane relays.
@@ -0,0 +1,9 @@
1
+ # New topics show their real names on the dashboard
2
+
3
+ When you create a topic in the agent's Telegram group, Telegram announces it with a small "topic created" note that carries the topic's name, and nothing else: no message text. On most of your machines the agent does not read Telegram directly. A small helper process (the lifeline) reads Telegram and hands each message to the agent's server. That helper threw away anything without text, so the "topic created" note, and with it the name, never reached the server. The dashboard then showed those conversations as "Conversation name unavailable", which is what you would have seen for several topics created today.
4
+
5
+ Now the helper passes the name on whenever it sees one: from the "topic created" or "topic renamed" note, and also from ordinary messages, because Telegram attaches the topic's creation note to each message in a topic. So the topics created today will get their names the next time anyone writes in them, and new topics get their names straight away.
6
+
7
+ Only the helper itself (which proves who it is with a secret) can set a name this way, so nothing else running on the machine can change what your topics are called.
8
+
9
+ A follow-up marks two deliberate catches (a failed rename display update is only logged) so the check that looks for silently swallowed errors can tell they are intentional.
@@ -0,0 +1,39 @@
1
+ # Side-effects review: topic names reach the server when the lifeline owns polling
2
+
3
+ ## Change
4
+ 1. `TelegramLifeline.processUpdate`: a topic-created or topic-renamed service message (no text) was dropped by `if (!msg.text) return;`. It is now passed on as a name-only forward (`text: ''`, `topicName`, `topicRenamed`, `nameOnly: true`). An ordinary message in a topic also carries the topic's name, because Telegram attaches the topic's creation message as `reply_to_message`; the lifeline now includes it as `topicName`.
5
+ 2. `POST /internal/telegram-forward`: after the existing lifeline-secret gate, a `topicName` is recorded with `TelegramAdapter.captureTopicName` only when the sender is lifeline-trusted and the request is not a mobile one. A name-only forward is acknowledged (`{ ok: true, nameOnly: true }`) and goes no further; an empty text without `nameOnly` is still a 400.
6
+ 3. `TelegramAdapter.captureTopicName(topicId, name, isEdit)`: the poller's existing capture logic, extracted so both paths share it. A creation name never overwrites a known name; a rename does and fires the existing rename handler. Names are trimmed and capped at 128 characters.
7
+
8
+ ## Evidence
9
+ On 2026-11-02, five sessions on Mama PC showed as "Conversation name unavailable" on the dashboard. Four were topics the operator created in Telegram that day (135187, 135556, 135758, 135786); neither the Studio (which polls through its lifeline) nor Mama PC had their names, and the Studio's message log had their messages. The lifeline's `processUpdate` returned early on any update without text.
10
+
11
+ ## 1. Over-block
12
+ None.
13
+
14
+ ## 2. Under-block
15
+ A topic created while the server is down: the name-only forward is skipped, and the name arrives with the topic's next message (which carries the creation message). A topic whose messages never reach this machine stays unnamed here, as before.
16
+
17
+ ## 3. Level-of-abstraction fit
18
+ The lifeline is the only component that sees the update; the adapter owns the name registry. Correct layers.
19
+
20
+ ## 4. Signal vs authority
21
+ Display data only. The name is accepted only from a lifeline-proven sender (the same gate that decides sender identity), so a session cannot rename topics shown to the operator.
22
+
23
+ ## 5. Interactions
24
+ The name-only forward returns before pairing-invite handling, message logging, sessions and the sentinel, so it can never be treated as a message. Older servers answer 400 to the name-only forward (empty text); the lifeline ignores the result. Older lifelines omit the fields; nothing changes.
25
+
26
+ ## 6. External surfaces
27
+ Topic names appear on the dashboard and in alerts instead of "Conversation name unavailable" or numbers.
28
+
29
+ ## 7. Multi-machine posture
30
+ The name is recorded on the machine whose lifeline polls (the lease holder). Other machines read it through the pooled topic list.
31
+
32
+ ## 8. Rollback cost
33
+ Revert the commit. Names already recorded stay, which is harmless.
34
+
35
+ ## Class closure
36
+ unbounded-self-action: n/a. One best-effort forward per topic service message received; no retry loop.
37
+
38
+ ## Follow-up
39
+ The two rename-handler catches moved into `captureTopicName` carry `@silent-fallback-ok` (the no-silent-fallbacks ratchet reads 20 lines after each catch, and in the new position those lines include fallback wording from the next function). `session-lifecycle-phase-3-wiring` pins the new source shape.