instar 1.3.1639 → 1.3.1640
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 +18 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/core/AppConversationStore.d.ts +39 -0
- package/dist/core/AppConversationStore.d.ts.map +1 -1
- package/dist/core/AppConversationStore.js +9 -0
- 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/AppReplyRelayInstaller.d.ts.map +1 -1
- package/dist/core/AppReplyRelayInstaller.js +2 -0
- package/dist/core/AppReplyRelayInstaller.js.map +1 -1
- package/dist/core/CodexLifecycleProductionComposition.d.ts +1 -1
- package/dist/core/CodexLifecycleProductionComposition.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +9 -1
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/SlackReplyRelayInstaller.d.ts.map +1 -1
- package/dist/core/SlackReplyRelayInstaller.js +2 -0
- package/dist/core/SlackReplyRelayInstaller.js.map +1 -1
- package/dist/core/types.d.ts +16 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/data/standards-guard-index.json +1 -1
- package/dist/data/standards-guard-index.meta.json +2 -2
- package/dist/data/standards-registry.meta.json +1 -1
- package/dist/feedback-factory/execute/ConfinedRunner.d.ts +4 -1
- package/dist/feedback-factory/execute/ConfinedRunner.d.ts.map +1 -1
- package/dist/feedback-factory/execute/ConfinedRunner.js +40 -8
- package/dist/feedback-factory/execute/ConfinedRunner.js.map +1 -1
- package/dist/feedback-factory/execute/FeedbackExecutorService.js +4 -4
- package/dist/feedback-factory/execute/FeedbackExecutorService.js.map +1 -1
- package/dist/feedback-factory/execute/executePolicy.d.ts +3 -0
- package/dist/feedback-factory/execute/executePolicy.d.ts.map +1 -1
- package/dist/feedback-factory/execute/executePolicy.js +3 -0
- package/dist/feedback-factory/execute/executePolicy.js.map +1 -1
- package/dist/messaging/messageOrigin.d.ts +94 -0
- package/dist/messaging/messageOrigin.d.ts.map +1 -0
- package/dist/messaging/messageOrigin.js +246 -0
- package/dist/messaging/messageOrigin.js.map +1 -0
- package/dist/messaging/slack/FileHandler.d.ts +3 -1
- package/dist/messaging/slack/FileHandler.d.ts.map +1 -1
- package/dist/messaging/slack/FileHandler.js +9 -1
- package/dist/messaging/slack/FileHandler.js.map +1 -1
- package/dist/messaging/slack/SlackAdapter.d.ts +9 -0
- package/dist/messaging/slack/SlackAdapter.d.ts.map +1 -1
- package/dist/messaging/slack/SlackAdapter.js +60 -4
- package/dist/messaging/slack/SlackAdapter.js.map +1 -1
- package/dist/messaging/slack/SlackOriginLedger.d.ts +97 -0
- package/dist/messaging/slack/SlackOriginLedger.d.ts.map +1 -0
- package/dist/messaging/slack/SlackOriginLedger.js +298 -0
- package/dist/messaging/slack/SlackOriginLedger.js.map +1 -0
- package/dist/messaging/telegram-origin/OriginAwareness.d.ts +3 -0
- package/dist/messaging/telegram-origin/OriginAwareness.d.ts.map +1 -1
- package/dist/messaging/telegram-origin/OriginAwareness.js +14 -0
- package/dist/messaging/telegram-origin/OriginAwareness.js.map +1 -1
- package/dist/messaging/telegram-origin/OriginStore.d.ts +5 -0
- package/dist/messaging/telegram-origin/OriginStore.d.ts.map +1 -1
- package/dist/messaging/telegram-origin/OriginStore.js +2 -1
- package/dist/messaging/telegram-origin/OriginStore.js.map +1 -1
- package/dist/messaging/telegram-origin/OriginStore.worker.js +1 -1
- package/dist/messaging/telegram-origin/OriginStore.worker.js.map +1 -1
- package/dist/messaging/telegram-origin/OriginStoreBackend.d.ts +14 -0
- package/dist/messaging/telegram-origin/OriginStoreBackend.d.ts.map +1 -1
- package/dist/messaging/telegram-origin/OriginStoreBackend.js +45 -0
- package/dist/messaging/telegram-origin/OriginStoreBackend.js.map +1 -1
- package/dist/messaging/telegram-origin/TelegramOriginService.d.ts +4 -0
- package/dist/messaging/telegram-origin/TelegramOriginService.d.ts.map +1 -1
- package/dist/messaging/telegram-origin/TelegramOriginService.js +10 -2
- package/dist/messaging/telegram-origin/TelegramOriginService.js.map +1 -1
- package/dist/native/InstarWedgeCanary.app/Contents/MacOS/InstarWedgeCanary +0 -0
- package/dist/native/instar-desktop-host +0 -0
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +3 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +3 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/mobileConversationRoutes.d.ts +17 -1
- package/dist/server/mobileConversationRoutes.d.ts.map +1 -1
- package/dist/server/mobileConversationRoutes.js +31 -6
- package/dist/server/mobileConversationRoutes.js.map +1 -1
- package/dist/server/mobileSlackRoutes.d.ts +13 -0
- package/dist/server/mobileSlackRoutes.d.ts.map +1 -1
- package/dist/server/mobileSlackRoutes.js +14 -1
- package/dist/server/mobileSlackRoutes.js.map +1 -1
- package/dist/server/originDisplayPreferences.d.ts.map +1 -1
- package/dist/server/originDisplayPreferences.js +19 -8
- package/dist/server/originDisplayPreferences.js.map +1 -1
- package/dist/server/replyOrigin.d.ts +39 -0
- package/dist/server/replyOrigin.d.ts.map +1 -0
- package/dist/server/replyOrigin.js +89 -0
- package/dist/server/replyOrigin.js.map +1 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +162 -4
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/scripts/blocking-decision-surfaces-baseline.json +3 -3
- package/src/data/builtin-manifest.json +66 -66
- 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 +4 -1
- package/src/templates/scripts/app-reply.sh +17 -0
- package/src/templates/scripts/slack-reply.sh +23 -0
- package/upgrades/1.3.1640.md +25 -0
- package/upgrades/feedback-confined-timeout.eli16.md +7 -0
- package/upgrades/side-effects/feedback-confined-timeout.md +80 -0
- package/upgrades/side-effects/message-origin-all-platforms.md +92 -0
|
@@ -126,6 +126,22 @@ DELIVERY_ID="${INSTAR_DELIVERY_ID:-}"
|
|
|
126
126
|
[[ "$DELIVERY_ID" =~ ^[0-9a-fA-F-]{16,64}$ ]] || DELIVERY_ID=""
|
|
127
127
|
[ -n "$DELIVERY_ID" ] || DELIVERY_ID=$(python3 -c 'import uuid; print(uuid.uuid4())' 2>/dev/null)
|
|
128
128
|
|
|
129
|
+
# message-origin-all-platforms §2: pass this session's origin credential so the server can record
|
|
130
|
+
# which harness and model wrote the reply. A surviving session can be re-enrolled after a server
|
|
131
|
+
# update without killing its process: read only its own server-published tmux credential, never
|
|
132
|
+
# print it. Absent or unverifiable, the server records what it can and the reply still sends.
|
|
133
|
+
if [ -n "${INSTAR_SESSION_NAME:-}" ] && command -v tmux >/dev/null 2>&1; then
|
|
134
|
+
ORIGIN_ENV_ROW=$(tmux show-environment -t "=${INSTAR_SESSION_NAME}" INSTAR_ORIGIN_TOKEN 2>/dev/null || true)
|
|
135
|
+
case "$ORIGIN_ENV_ROW" in
|
|
136
|
+
INSTAR_ORIGIN_TOKEN=ior1_*) INSTAR_ORIGIN_TOKEN="${ORIGIN_ENV_ROW#INSTAR_ORIGIN_TOKEN=}" ;;
|
|
137
|
+
esac
|
|
138
|
+
unset ORIGIN_ENV_ROW
|
|
139
|
+
fi
|
|
140
|
+
case "${INSTAR_ORIGIN_TOKEN:-}" in
|
|
141
|
+
ior1_*) ;;
|
|
142
|
+
*) INSTAR_ORIGIN_TOKEN="" ;;
|
|
143
|
+
esac
|
|
144
|
+
|
|
129
145
|
set +e
|
|
130
146
|
RESPONSE=$(curl -sS --connect-timeout 3 --max-time "$MAX_TIME" -w "\n%{http_code}" -X POST \
|
|
131
147
|
"http://localhost:${PORT}/app/session-reply" \
|
|
@@ -133,6 +149,7 @@ RESPONSE=$(curl -sS --connect-timeout 3 --max-time "$MAX_TIME" -w "\n%{http_code
|
|
|
133
149
|
${AUTH:+-H "Authorization: Bearer $AUTH"} \
|
|
134
150
|
${AGENT_ID:+-H "X-Instar-AgentId: $AGENT_ID"} \
|
|
135
151
|
${INSTAR_BIND_TOKEN:+-H "X-Instar-Bind-Token: $INSTAR_BIND_TOKEN"} \
|
|
152
|
+
${INSTAR_ORIGIN_TOKEN:+-H "X-Instar-Origin-Session: $INSTAR_ORIGIN_TOKEN"} \
|
|
136
153
|
${DELIVERY_ID:+-H "X-Instar-DeliveryId: $DELIVERY_ID"} \
|
|
137
154
|
-d "$BODY_JSON")
|
|
138
155
|
CURL_STATUS=$?
|
|
@@ -125,6 +125,22 @@ else
|
|
|
125
125
|
fi
|
|
126
126
|
|
|
127
127
|
# Send via Instar server
|
|
128
|
+
# message-origin-all-platforms §2: pass this session's origin credential so the server can record
|
|
129
|
+
# which harness and model wrote the reply. A surviving session can be re-enrolled after a server
|
|
130
|
+
# update without killing its process: read only its own server-published tmux credential, never
|
|
131
|
+
# print it. Absent or unverifiable, the server records what it can and the reply still sends.
|
|
132
|
+
if [ -n "${INSTAR_SESSION_NAME:-}" ] && command -v tmux >/dev/null 2>&1; then
|
|
133
|
+
ORIGIN_ENV_ROW=$(tmux show-environment -t "=${INSTAR_SESSION_NAME}" INSTAR_ORIGIN_TOKEN 2>/dev/null || true)
|
|
134
|
+
case "$ORIGIN_ENV_ROW" in
|
|
135
|
+
INSTAR_ORIGIN_TOKEN=ior1_*) INSTAR_ORIGIN_TOKEN="${ORIGIN_ENV_ROW#INSTAR_ORIGIN_TOKEN=}" ;;
|
|
136
|
+
esac
|
|
137
|
+
unset ORIGIN_ENV_ROW
|
|
138
|
+
fi
|
|
139
|
+
case "${INSTAR_ORIGIN_TOKEN:-}" in
|
|
140
|
+
ior1_*) ;;
|
|
141
|
+
*) INSTAR_ORIGIN_TOKEN="" ;;
|
|
142
|
+
esac
|
|
143
|
+
|
|
128
144
|
set +e
|
|
129
145
|
RESPONSE=$(curl -sS --connect-timeout 3 --max-time 35 -w "\n%{http_code}" -X POST \
|
|
130
146
|
"$REPLY_URL" \
|
|
@@ -132,6 +148,7 @@ RESPONSE=$(curl -sS --connect-timeout 3 --max-time 35 -w "\n%{http_code}" -X POS
|
|
|
132
148
|
${AUTH:+-H "Authorization: Bearer $AUTH"} \
|
|
133
149
|
${AGENT_ID:+-H "X-Instar-AgentId: $AGENT_ID"} \
|
|
134
150
|
${INSTAR_BIND_TOKEN:+-H "X-Instar-Bind-Token: $INSTAR_BIND_TOKEN"} \
|
|
151
|
+
${INSTAR_ORIGIN_TOKEN:+-H "X-Instar-Origin-Session: $INSTAR_ORIGIN_TOKEN"} \
|
|
135
152
|
${DELIVERY_ID:+-H "X-Instar-DeliveryId: $DELIVERY_ID"} \
|
|
136
153
|
-d "$BODY_JSON")
|
|
137
154
|
CURL_STATUS=$?
|
|
@@ -152,6 +169,12 @@ if [ "$HTTP_CODE" = "200" ]; then
|
|
|
152
169
|
else
|
|
153
170
|
echo "Sent ${#MESSAGE} chars to channel ${CHANNEL_ID}"
|
|
154
171
|
fi
|
|
172
|
+
elif [ "$HTTP_CODE" = "503" ] && printf '%s' "$BODY" | grep -q 'origin-recording-unavailable'; then
|
|
173
|
+
# message-origin-all-platforms §3: this machine could not record which machine and model wrote
|
|
174
|
+
# the reply, so nothing was sent to Slack. A fixed notice tells the conversation.
|
|
175
|
+
echo "NOT SENT (HTTP 503): the server could not record this reply's origin, so Slack was not contacted." >&2
|
|
176
|
+
echo " Nothing was delivered. Resend once the server reports origin recording is working (GET /health -> messageOrigin)." >&2
|
|
177
|
+
exit 1
|
|
155
178
|
elif [ "$HTTP_CODE" = "408" ]; then
|
|
156
179
|
# Request timeout on the server side — the outbound path (tone gate + Slack API)
|
|
157
180
|
# exceeded the route's budget. The actual send may have completed anyway.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Every message the agent sends now records which machine, harness ("door", such as Claude Code or Codex) and model wrote it on app conversations and Slack too, not only on Telegram. An app reply is stored together with its origin. Every Slack post, edit and file share writes a small origin record before it leaves; if that record cannot be written the message is not sent, and one short pre-recorded notice tells the conversation, so the agent is never silent. The agent's own Slack replies carry the same one-line origin footer Telegram replies have, under the same show/hide setting.
|
|
9
|
+
|
|
10
|
+
The Instar app's history reads (app conversations, Slack and Telegram) now return an `origin` block on each agent message with the machine, harness, model, how each was known, and the agent's current show/hide setting. `recorded: null` means this machine cannot tell, never "not recorded". On an agent without Telegram the setting lives at the top level of the config (`messageOrigin.display`).
|
|
11
|
+
|
|
12
|
+
## What to Tell Your User
|
|
13
|
+
|
|
14
|
+
My messages now carry where they came from on every platform you use with me: which of your machines wrote them, which tool I was running in, and which model. It shows by default. If you would rather not see it, just tell me and I will hide it; it is still recorded either way. Slack replies get the same small footer Telegram already has. The app shows it once its next update reads the new data.
|
|
15
|
+
|
|
16
|
+
## Summary of New Capabilities
|
|
17
|
+
|
|
18
|
+
- App and Slack replies record machine, harness and model, like Telegram.
|
|
19
|
+
- Phone history reads return the origin of each agent message, with the display setting.
|
|
20
|
+
- Slack replies carry the origin footer.
|
|
21
|
+
- A Slack message is never sent without its origin record; a pre-recorded notice keeps the conversation informed.
|
|
22
|
+
|
|
23
|
+
## Evidence
|
|
24
|
+
|
|
25
|
+
Unit tests cover the evidence model, the reply-origin ladder, the Slack ledger (refusal, recovery, rotation, the reserved notice), the app store field and its replication, the batched Telegram lookup and its index. An integration test drives the three phone reads and the Slack reply route through the production device authenticator; an end-to-end test boots the server with no Telegram, replies in an app conversation and reads the recorded origin back from the phone route.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Feedback worker test timeout cleanup
|
|
2
|
+
|
|
3
|
+
When a feedback fix changes a core file, its related tests can cover much of Instar. The old 20 minute limit stopped one SecretStore attempt before it could open a pull request. The sandbox wrapper exited, but its test children kept the output pipe open, so the executor stayed stuck until those children were stopped separately.
|
|
4
|
+
|
|
5
|
+
The related test step now has a 45 minute limit. All other confined commands keep their 20 minute limit. When either limit is reached, the runner kills the original process group and records the timeout immediately, even if a child has separated from that group and kept an output pipe open. Native Windows execution is refused because this process containment is supported only on macOS and Linux. The worker still has to pass its old-code test, new-code test, lint, related tests, independent review, local verification, and CI before a merge.
|
|
6
|
+
|
|
7
|
+
This change does not make the SecretStore patch safe by itself. That patch has a separate independent data loss finding and needs correction before merge.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Side-Effects Review — bounded feedback test timeout
|
|
2
|
+
|
|
3
|
+
**Version / slug:** feedback-confined-timeout
|
|
4
|
+
**Date:** 2026-11-03
|
|
5
|
+
**Author:** Echo
|
|
6
|
+
**Second-pass reviewer:** feedback_gate_review (concurred)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
The feedback executor's confined command runner now returns a timeout or output-limit failure even when a descendant retains its output pipe. It kills the original POSIX process group, destroys its local pipes, and settles without waiting for Node's `close` event. `vitest related` gets 45 minutes; other gates remain at 20 minutes. The approved execution spec records the exception. Native Windows remains unsupported by the runner. This changes the executor's resource-bound decision, not review or merge policy.
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- `runCapped` timeout and output cap — modified — settle with a failure at the fixed limit.
|
|
15
|
+
- `FeedbackExecutorService` related gate budget — modified — use 45 minutes only for this gate.
|
|
16
|
+
- `SandboxRuntimeRunner` platform support — modified — refuse direct `run()` on unsupported platforms, matching `available()`.
|
|
17
|
+
|
|
18
|
+
## 1. Over-block
|
|
19
|
+
|
|
20
|
+
A legitimate related test that needs more than 45 minutes still fails. That is an intentional resource ceiling inside the six-hour attempt lease. Native Windows is refused, matching the preexisting availability refusal; there is no new supported Windows execution path to block.
|
|
21
|
+
|
|
22
|
+
## 2. Under-block
|
|
23
|
+
|
|
24
|
+
A descendant that starts a new process group can continue running after the timeout result. The sandbox's filesystem and network policy still constrain it, but process-group SIGKILL cannot terminate that escaped group. The result itself is bounded. This residual needs a separate process-containment design if escaped children are observed to persist.
|
|
25
|
+
|
|
26
|
+
## 3. Level-of-abstraction fit
|
|
27
|
+
|
|
28
|
+
The runner owns process lifetime and pipe closure; the executor owns per-gate time budgets. A reviewer cannot repair a stuck low-level Promise. The change keeps the existing failed-at-limit result flowing into the executor's attempt state machine.
|
|
29
|
+
|
|
30
|
+
## 4. Signal vs authority compliance
|
|
31
|
+
|
|
32
|
+
Required reference: [Signal vs Authority](../../docs/signal-vs-authority.md). This is deterministic resource-limit enforcement, not a judgment about feedback content or agent intent. The runner reports `timedOut` or `outputCapped` to the existing executor authority; it does not make a new semantic block/allow decision. The platform refusal is a safety invariant for unavailable containment.
|
|
33
|
+
|
|
34
|
+
## 4b. Judgment-point check
|
|
35
|
+
|
|
36
|
+
No new static heuristic at a competing-signals decision point. The wall-clock budget is an explicit invariant recorded in the approved execution spec.
|
|
37
|
+
|
|
38
|
+
## 5. Interactions
|
|
39
|
+
|
|
40
|
+
- **Shadowing:** The 45-minute related budget replaces the generic 20-minute budget only at the related-test call; base, head, lint, canary, and dependency calls retain the generic budget.
|
|
41
|
+
- **Double-fire:** A timeout and `close` can race. The `settled` flag allows only one result; the timer is cleared when settled.
|
|
42
|
+
- **Races:** A detached descendant may outlive clone cleanup. It remains inside the sandbox policy, but its process lifetime is not guaranteed by this patch. The executor records the limit failure without waiting on it.
|
|
43
|
+
- **Feedback loops:** No new retry or auto-merge trigger is added. A failed attempt remains subject to existing caps and review policy.
|
|
44
|
+
|
|
45
|
+
## 6. External surfaces
|
|
46
|
+
|
|
47
|
+
An executor attempt that previously remained stuck in `verifying` now records a limit failure. A related test may consume up to 45 minutes. Feedback API result shapes and Telegram message formats do not change. No operator-facing action is added.
|
|
48
|
+
|
|
49
|
+
## 6b. Operator-surface quality
|
|
50
|
+
|
|
51
|
+
No operator surface — not applicable.
|
|
52
|
+
|
|
53
|
+
## 7. Multi-machine posture
|
|
54
|
+
|
|
55
|
+
Machine-local by design: a confined command and its process group exist on the machine that owns the attempt. The attempt's state is recorded by the existing feedback executor on the drain owner. This change emits no user-facing notices, holds no new durable state, and generates no URLs. Topic transfer does not affect the local timeout result.
|
|
56
|
+
|
|
57
|
+
## 8. Rollback cost
|
|
58
|
+
|
|
59
|
+
Revert the code and spec amendment, then ship a patch. No data migration or agent state repair is needed. Rollback restores the 20-minute related limit and the known risk of a stuck `verifying` attempt when a descendant holds output pipes.
|
|
60
|
+
|
|
61
|
+
## Conclusion
|
|
62
|
+
|
|
63
|
+
The reviewer identified two gaps in the first draft: detached pipe holders could still stall completion, and the 45-minute exception conflicted with the spec. Both are corrected. The reviewer concurred with the revised bounded-result fix. The escaped-descendant lifetime risk is explicit above and does not negate the bounded-result claim.
|
|
64
|
+
|
|
65
|
+
## Second-pass review
|
|
66
|
+
|
|
67
|
+
**Reviewer:** feedback_gate_review
|
|
68
|
+
**Independent read of the artifact:** concur
|
|
69
|
+
|
|
70
|
+
The reviewer found no remaining blocker for bounded timeout completion and confirmed the escaped-descendant residual is documented.
|
|
71
|
+
|
|
72
|
+
## Evidence pointers
|
|
73
|
+
|
|
74
|
+
- Live attempt `feedback-e800bf35-6381-46e7-a769-1530c55ac7ac:a6` failed `limit:command-wall-clock` after its old runner required manual descendant termination.
|
|
75
|
+
- Unit regression covers ordinary and detached descendants retaining stdout. Focused runner/executor tests: 154 passed.
|
|
76
|
+
- Integration route tests: 9 passed. E2E executor lifecycle tests: 2 passed. Build and lint passed. Full three-stage suite is running.
|
|
77
|
+
|
|
78
|
+
## Class-Closure Declaration (display-only mirror)
|
|
79
|
+
|
|
80
|
+
No agent-authored-artifact defect. The timeout's `killTree` is a one-shot response to a single confined command crossing its fixed limit, not a self-triggered loop or retry controller. Negative declaration: `defectClass: unbounded-self-action`, `closure: n/a`, reason: one-shot per-command resource enforcement, with no action scheduled by the result itself.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Side-Effects Review — Message origin on every platform (app, Slack, phone reads)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `message-origin-all-platforms`
|
|
4
|
+
**Date:** `2026-11-03`
|
|
5
|
+
**Author:** `Echo`
|
|
6
|
+
**Second-pass reviewer:** `independent subagent (see below)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Tier 2. Spec: `docs/specs/message-origin-all-platforms.md` (3 review rounds, cap 3; cross-model `codex-cli:gpt-6-astra`). Every agent reply in a native app conversation is stored with its origin (machine, harness, model and how each was known) in the same append; every Slack post, edit, ephemeral and file share writes an origin row to `.instar/state/slack-origins.jsonl` before it leaves and is refused when that row cannot be written (a pre-recorded per-conversation notice keeps the agent reachable); the phone's three history reads return `origin` on agent rows with the agent's current display bits; Telegram rows join the existing origin store through one new batched, indexed, read-only lookup. Slack session replies get the same one-line footer Telegram has. New files: `src/messaging/messageOrigin.ts`, `src/messaging/slack/SlackOriginLedger.ts`, `src/server/replyOrigin.ts`. Changed: `SlackAdapter`, `FileHandler`, `routes.ts` (app and Slack reply routes, Telegram topic history, /health), `AppConversationStore` (plus the replicated wire field), mobile conversation and Slack routes, `AgentServer` and `server.ts` wiring, the origin store (index and method), `TelegramOriginService` (`sessionProducer` lifted out of `runWithSessionToken`), the display-settings route, relay scripts and installers, the CLAUDE.md template and migration. Effort: 3 spec rounds; build in one pass.
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- **Slack send refusal when the origin intent cannot be written** — added. Deterministic: the local append landed or not. Scoped to an I/O failure; bad evidence is recorded as `unknown`, never refused.
|
|
15
|
+
- **Evidence ladder** — added, evidence only, never a gate.
|
|
16
|
+
- **Display bits** — presentation only.
|
|
17
|
+
|
|
18
|
+
## 1. Over-block
|
|
19
|
+
|
|
20
|
+
A Slack send is refused when the ledger file cannot be written (disk full, permissions). That refuses legitimate messages in that state, by design (the operator's "ALWAYS recorded"). Bounded: the refusal retries the write on the next send after 5 s, so it lifts the moment the disk does; a pre-recorded notice tells an affected conversation once per process run, only for a refused post, and only if a send to that conversation succeeded earlier in the run (edits, ephemeral messages and file shares get no notice); the operator exception `messageOrigin.recording.slackFailClosed: false` turns refusal into reported pass-through. Any failed ledger write (an intent, a receipt or a reserve) arms the 5-second window for every conversation, because it is the same disk. A test harness or adapter built with an unwritable state directory would refuse every Slack send: the adapter test files touched here pass.
|
|
21
|
+
|
|
22
|
+
## 2. Under-block
|
|
23
|
+
|
|
24
|
+
- WhatsApp and iMessage replies record nothing (ACT-023).
|
|
25
|
+
- A Slack message whose receipt write failed has a record the phone cannot link (`recorded: null`, `unlinked`).
|
|
26
|
+
- `SlackLiveSender` (the live-test harness, a separate test identity) bypasses the chokepoint by design.
|
|
27
|
+
- `SlackAdapter.send()` posts long messages in chunks, one call each; a refusal on a later chunk leaves earlier chunks posted. No reply route uses it.
|
|
28
|
+
- A reply whose evidence scope does not match the adapter's target (an unusual thread value) is recorded as server automation, not refused.
|
|
29
|
+
- On an agent without Telegram, harness and model come from the launch record (`configured`), not a runtime observation.
|
|
30
|
+
- Messages sent before this change read `false` (app, this machine), `not-found` (Telegram) or `not-retained` (Slack).
|
|
31
|
+
|
|
32
|
+
## 3. Level-of-abstraction fit
|
|
33
|
+
|
|
34
|
+
Recording sits at each platform's send chokepoint (the app reply append; `SlackAdapter.formattedApiCall` and `FileHandler.uploadFile`), not in agent prose or a hook. The evidence model and the session-producer derivation are the Telegram ones, reused (`OriginEvidence`, `sessionProducer`). The Telegram store is not duplicated; it gains one read method and one index.
|
|
35
|
+
|
|
36
|
+
## 4. Signal vs authority compliance
|
|
37
|
+
|
|
38
|
+
The only blocking authority added is the Slack "no record, no send" rule: deterministic (did a local append succeed), the same rule the existing standard applies to Telegram, with reachability preserved and an operator exception. Origin evidence itself is information: nothing reads it to decide anything; the bind token, not the evidence, decides which conversation a reply may reach.
|
|
39
|
+
|
|
40
|
+
## 4b. Judgment-point check
|
|
41
|
+
|
|
42
|
+
No competing signals: a file append succeeded or it did not. Not a judgment point.
|
|
43
|
+
|
|
44
|
+
## 5. Interactions
|
|
45
|
+
|
|
46
|
+
- `handleSlackReply` (both Slack reply routes): the origin scope wraps only the adapter send; the 503 branch sits before the generic 500 and after the 408 timeout branch, so the redrive and delivery-id rules are unchanged.
|
|
47
|
+
- Outbound review: the footer is added after `checkOutboundMessage` (it is not part of the reviewed authored text) and passes the deterministic credential wall; the LLM tone gate never sees it.
|
|
48
|
+
- App store replication: `agentOrigin` added to the wire allowlist; the receiver re-validates it and keeps it only when its machine id matches the event's.
|
|
49
|
+
- Telegram store worker: one indexed read per phone history page, bounded at 750 ms by the caller.
|
|
50
|
+
- Presence standby: the outage notice never calls `onMessageLogged`.
|
|
51
|
+
- Display-settings route: on an agent with no enabled Telegram entry it now reads and writes the top-level setting instead of answering 503.
|
|
52
|
+
|
|
53
|
+
## 6. External surfaces
|
|
54
|
+
|
|
55
|
+
- Slack users see a new italic footer line on the agent's own replies (on by default; the same display bits hide it).
|
|
56
|
+
- The phone's history rows gain an `origin` field (additive; older app builds ignore it). Values: display names, harness ids, model ids, statuses. Never tokens, session ids, paths, refs or channel ids.
|
|
57
|
+
- `/health` (authed) gains `messageOrigin` counters.
|
|
58
|
+
- `app-reply.sh` and `slack-reply.sh` send one more header; `slack-reply.sh` prints `NOT SENT` on the new 503.
|
|
59
|
+
|
|
60
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
61
|
+
|
|
62
|
+
- App origin: replicated with the event through the existing app store replication; a peer's record must carry its own machine id.
|
|
63
|
+
- Slack ledger: proxied-on-read through the machine that sent the message. Every Slack read names the machine that lists the conversation, and a message another machine sent reads `null`, `other-machine` here (the Slack connection moves with the serving lease).
|
|
64
|
+
- Telegram: per-machine store; pool reads fan out with `includeOrigin=1` and each machine joins its own rows; a peer's recorded origin fills a local `null` on collision.
|
|
65
|
+
- Display bits: read on the machine answering, never taken from a peer.
|
|
66
|
+
|
|
67
|
+
## 8. Rollback cost
|
|
68
|
+
|
|
69
|
+
Revert the release. Recorded fields are additive; older code ignores `agentOrigin` and never reads the Slack ledger. The Slack refusal alone has a live operator switch. No state repair.
|
|
70
|
+
|
|
71
|
+
## Conclusion
|
|
72
|
+
|
|
73
|
+
Ships live for every agent, as asked. The one new refusal is narrow, loud, recoverable without a restart and switchable by the operator.
|
|
74
|
+
|
|
75
|
+
## Second-pass review (if required)
|
|
76
|
+
|
|
77
|
+
**Reviewer:** independent subagent (Claude, fresh context), reading the artifact, the spec and the diff.
|
|
78
|
+
**Independent read of the artifact: concur**
|
|
79
|
+
|
|
80
|
+
Notes raised and handled: (1) the credential lookup was unbounded and the app route's evidence call sat outside its try: now bounded at 1.5 s and the ladder never throws; (2) a reserve or report error after a written intent could count as a refusal: moved after the write; (3) the notice wording overstated it: §1 corrected; (4) Telegram join reason labels: `not-found` with no store, `lookup-failed` on a store error; (5) a thread-value edge and the chunked `send()` path: aligned the scope with the adapter and noted in §2.
|
|
81
|
+
|
|
82
|
+
## Evidence pointers
|
|
83
|
+
|
|
84
|
+
- Unit: `tests/unit/messageOrigin.test.ts`, `tests/unit/SlackOriginLedger.test.ts`, `tests/unit/replyOrigin.test.ts`, `tests/unit/app-conversation-agent-origin.test.ts`, `tests/unit/telegram-origin/lookup-origin-evidence.test.ts`, `tests/unit/PostUpdateMigrator-messageOriginAllPlatforms.test.ts`.
|
|
85
|
+
- Integration (production device authenticator): `tests/integration/message-origin-all-platforms.test.ts`.
|
|
86
|
+
- E2E alive: `tests/e2e/message-origin-all-platforms-alive.test.ts`.
|
|
87
|
+
|
|
88
|
+
## Found in testing
|
|
89
|
+
|
|
90
|
+
The integration test caught one defect before push: `detectOutboundCredential` returns `{ detected }`, and the footer check treated the object as truthy, so the Slack footer was always dropped. Fixed to read `.detected`; the integration test asserts the footer reaches the adapter.
|
|
91
|
+
|
|
92
|
+
Live Slack contract tests were not run: Echo's Slack bot token was revoked on 2026-07-01. The changed adapter files carry CONTRACT-EVIDENCE: EXEMPT markers stating that no Slack API method, parameter or response field changed.
|