Prepare the FLUID interface for HelixForge Telegram publishing

Adds the governing intent, the R-1 contract and descriptor, the adapter's
connector contract, the observation and redaction configuration, a
runbook, and the seed evidence for the first experiment: a measured
pressure record and two competing hypotheses about how a long hall entry
should reach a reader.

The interface is declared at FLUID-2 deliberately. It publishes under
HelixForge's name to an audience that did not consent to being
experimented on carelessly, and the cost of a bad post is reputational
rather than recoverable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014KmVxhJ35tCo7rE7UnLwWu

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1116572@bnt-lap001
Assistant-Session: 8ba9bb93-a72a-4883-b189-2499cce5c400
This commit is contained in:
tegwick 2026-09-04 11:07:11 +02:00
parent 87795446e2
commit 49b7458dfe
13 changed files with 3010 additions and 1 deletions

507
InterfaceEvolutionIntent.md Normal file
View file

@ -0,0 +1,507 @@
# Interface Evolution Intent — helix-forge-telegram-publishing
**Document version:** IEI-1
**Status:** active
**Last reviewed:** 2026-09-04
---
## 1. Purpose
This document defines the evolutionary intent and governance boundary of the
FLUID interface that publishes HelixForge material to Telegram.
It is subordinate to `helix-forge/INTENT.md` and does not redefine HelixForge's
mission. It answers one question:
> How may this interface evolve while remaining faithful to the service it
> represents?
The FLUID Daimon must treat this document as a constitutional constraint.
---
## 2. Parent Intent
**Parent service/repository:** `helix-forge`
**Parent intent document:** `helix-forge/INTENT.md`
**Relevant parent mission:** HelixForge exists to turn intent into structure,
structure into capability, and capability into lasting progress. The Hall of
Helix is where that work's participants are acknowledged. This interface carries
those acknowledgements outward.
---
## 3. Interface Identity
**Interface name:** HelixForge Telegram Publishing
**Interface identifier:** `helix-forge-telegram-publishing`
**Current operational authority mode:** FLUID-2
FLUID-2 (Advisory) is a deliberate choice, not a starting point to be outgrown
on a schedule. The Daimon may observe, classify pressure and propose
adaptations. It may not generate implementations, run experiments unattended, or
promote anything. This interface publishes under HelixForge's name to an
audience that did not consent to being experimented on carelessly, and the cost
of a bad post is reputational rather than recoverable.
Raising the mode is a governance change under §20, not an operational decision.
**Protocols / interface styles:** REST over HTTP (JSON)
---
## 4. Mission
### Mission statement
> Make hall-of-helix entries reliably and faithfully available to a Telegram
> audience, without the people writing them having to think about Telegram.
An entry is written once, as Markdown, in `hall-of-helix/entries/`. Everything
about how it becomes a Telegram post — length, formatting, media, threading —
is this interface's problem and nobody else's.
### Success looks like
- A finished hall entry reaches the channel without a human reformatting it.
- The published form is faithful: no entry is silently truncated, and no
attribution is lost.
- Publishing twice does not post twice.
- When Telegram's constraints and the entry's shape conflict, the conflict
surfaces as recorded pressure rather than as a mangled post.
---
## 5. Intended Consumers
### Primary consumer cohorts
| Cohort | Description | Importance | Notes |
|---|---|---|---|
| `hall-publishing-jobs` | Automation in `hall-of-helix` that publishes finished entries | High | The reason this interface exists |
| `helix-forge-agents` | Agent sessions announcing their own completed work | High | Highest-volume caller; supplies the richest pressure signal |
| `operators` | Humans publishing or correcting by hand | Medium | Low volume, high consequence |
### Secondary cohorts
| Cohort | Description | Importance | Notes |
|---|---|---|---|
| `telegram-subscribers` | Readers of the channel | Medium | Not API consumers; observed through engagement, never individually |
### Explicitly unsupported cohorts
- Third-party services outside the HelixForge boundary.
- Anonymous or unauthenticated callers.
- Anything seeking to publish content it did not author.
---
## 6. Capability Domain
### In scope
- Publishing hall-of-helix entries to a Telegram channel (Canon PUB-01).
- Rendering an entry into Telegram's message forms (Canon PUB-02, MED-02).
- Publication state: what was published, when, as which message.
- Idempotency and correction of previously published entries.
- Bot identity presentation (Canon BOT-01).
### Conditionally in scope
Capabilities that may be introduced if evidence justifies them:
- Scheduling a publication for a later time.
- Publishing other HelixForge artifacts, such as release notes.
- Attaching entry visuals as media (Canon MED-01).
- A linked discussion group for comments.
### Out of scope
- Inbound bot commands and conversation (Canon BOT-02, AGT-01). A second
revision family may add this later; it is not this interface today.
- Payments and paid content (Canon PAY-*).
- Mini Apps (Canon APP-*).
- Moderation and membership management (Canon COM-02, COM-03).
- Any capability requiring a Telegram user account rather than a bot.
---
## 7. Responsibility Boundaries
### Responsibilities owned by this interface
- Representation: how an entry becomes one or more Telegram messages.
- Composition: assembling an entry, its metadata and its visual.
- Validation: refusing an entry that cannot be published faithfully.
- Routing: which revision serves a publication request.
- Idempotency: recognising an entry it has already published.
### Responsibilities owned by backend systems
| Backend | Responsibility | Boundary |
|---|---|---|
| Telegram Bot API | Message delivery, channel state, subscriber identity | FLUID may request delivery. It may not assume a limit will move, and must treat platform constraints as fixed. |
| `hall-of-helix` | Entry content, its correctness, and whether it is finished | FLUID may read entries. It must never edit one to make it publishable. |
| OpenBao | Custody of the bot token | FLUID receives a credential. It never stores, logs or forwards one. |
### Explicit non-authority
The Daimon must not autonomously:
- create, rename or delete Telegram channels, groups or bots;
- change channel membership, administrators or permissions;
- read, rotate or re-scope the bot token;
- edit or truncate a hall-of-helix entry to fit a platform constraint;
- publish to any chat other than those declared in the routing configuration;
- post on behalf of a person who has not asked for it.
The last of these matters most. A hall entry is a first-person account someone
wrote about their own work. Publishing it is an act taken on their behalf, and
that authority comes from their having written it — not from the interface
finding it convenient.
---
## 8. Consumer Expectation Model
### Concepts consumers should naturally understand
- An **entry** is the unit of publication, identified by its hall entry id.
- Publishing is **idempotent** on that id.
- A **publication** has state: pending, published, failed, superseded.
### Interaction patterns considered natural
- Publish one finished entry, then check its state.
- Re-publish the same entry after a correction and get the same message updated.
- List what has been published to reconcile against the hall.
### Expectations that should be discouraged
- That Telegram formatting is Markdown. It is not, and entries that assume so
will render wrongly.
- That an entry appears instantly. Rate limits are real.
- That a long entry arrives as one message. It cannot, and the interface's job
is to make that invisible rather than to pretend otherwise.
---
## 9. Optimization Preferences
### Active optimization regime
`ADOPTION` — the channel has no audience yet. Until it does, discoverability and
client utility matter more than efficiency, and cost is negligible at this
volume.
### Fitness priorities
| Dimension | Priority | Target / Constraint | Notes |
|---|---:|---|---|
| Correctness | High | No entry published in a form that loses content | Hard guardrail |
| Client utility | High | One call publishes one entry | The reason the interface exists |
| Reliability | High | Publication succeeds or reports why | |
| Discoverability | High | A reader can reach the full entry | Drives the first experiment |
| Security | High | Token never leaves its custody | Hard guardrail |
| Compatibility | Medium | Versioned evolution | Few consumers, all internal |
| Simplicity | Medium | Prefer one publication concept | |
| Performance | Low | Publication is not latency-sensitive | Minutes are acceptable |
| Operational cost | Low | Negligible at this volume | |
### Automatic recommendation policy
`advisory` — the Daimon may recommend regime changes. It may not enact one.
---
## 10. Complexity Budget
### Complexity dimensions
| Dimension | Current | Preferred | Maximum | Scoring method |
|---|---:|---:|---:|---|
| Surface area (operations) | 3 | 4 | 8 | Count of declared operations |
| Concept count | 2 | 3 | 5 | entry, publication |
| Parameter dimensionality | 4 | 6 | 12 | Declared parameters across operations |
| Semantic overlap | 0 | 0 | 1 | Operations meaning the same thing |
| Alternative-path count | 1 | 1 | 3 | Ways to achieve one outcome |
| Backend composition count | 1 | 2 | 3 | Backends per operation |
| Active revision count | 1 | 2 | 3 | Concurrently routable revisions |
### Complexity policy
- New capability should replace or generalize existing capability where
reasonable.
- Semantic duplication is rejected unless justified in writing.
- Repeated special-case pressure triggers an abstraction review rather than
another endpoint.
- A candidate exceeding a hard maximum must not be promoted.
- Presentation adaptations do not consume surface-area budget: they change how
an entry is rendered, not what the interface offers.
---
## 11. Compatibility Policy
### Compatibility posture
`VERSIONED`
### Rules
- Published revision semantics must not change silently.
- Breaking changes require a successor revision.
- Every consumer is internal and known, so a breaking change is a coordination
problem rather than a public one — but it is still a successor revision.
- A change to how an entry is *rendered* is a presentation adaptation and does
not break the contract, even though readers will notice it.
### Coexistence expectations
**Default maximum concurrent major revisions:** 2
**Default deprecation period:** 30 days after the last call from any consumer
**Migration assistance required:** yes — this interface has few enough consumers
that migration means telling their owners directly.
---
## 12. Revision and Experiment Policy
### Allowed concurrent exposure
| Revision class | Production traffic allowed? | Max share | Approval required? |
|---|---|---:|---|
| stable | yes | 100% | no |
| candidate | yes | 30% | yes, human |
| experiment | yes | 20% | yes, human |
| deprecated | yes | 100% | no |
| retired | no | 0% | n/a |
### Evolution velocity controls
The Daimon should slow or freeze promotion when:
- any entry has been published in a form that lost content;
- the bot has been rate-limited by Telegram in the last 24 hours;
- a security event has touched the token;
- an experiment is already running. Concurrency limit: 1.
The Daimon may accelerate experimentation when:
- the channel has more than 100 subscribers, giving experiments real power;
- the last three experiments completed without a guardrail breach.
**Note on experiment power.** At current volume — roughly one entry published
per week — an experiment needs months to reach a decidable sample. The evaluator
will report INCONCLUSIVE, and that is the correct answer. Do not shorten windows
to force a verdict.
---
## 13. Hypothesis Policy
### Minimum evidence for hypothesis creation
- At least 5 independent observations, or one unavoidable structural constraint.
- Observation window of at least 14 days for behavioural pressure.
- A structural constraint (a platform limit) may found a hypothesis immediately;
it does not need repetition to be real.
### Required hypothesis fields
observation, explanation, proposed adaptation, expected outcome, measurement,
success criteria, guardrails, complexity impact, risk, expected learning value.
### Competing hypotheses
`encouraged` — the first pressure this interface faces has at least two
defensible answers, and choosing between them by argument rather than evidence
is exactly the habit FLUID exists to break.
### Reach preference
Prefer the smallest sufficient adaptation. This interface is small enough that a
high-reach abstraction would be speculative; revisit when there are more than
three distinct kinds of pressure.
---
## 14. Adaptation Authority
| Adaptation class | Observe | Propose | Generate | Experiment | Promote |
|---|---:|---:|---:|---:|---:|
| Presentation | yes | yes | no | no | no |
| Contract | yes | yes | no | no | no |
| Composition | yes | yes | no | no | no |
| Implementation | yes | yes | no | no | no |
| Requirement escalation | yes | yes | yes | n/a | n/a |
Every `Generate`, `Experiment` and `Promote` column is `no`, which is what
FLUID-2 means. Requirement escalation is the exception: the Daimon may write a
structured backend requirement, because that artifact is a request to someone
else and carries no authority of its own.
### Absolute prohibitions
The Daimon must not autonomously:
- expand authorization scope;
- weaken tenant isolation;
- bypass deterministic security controls;
- reinterpret legal constraints;
- create backend authority it does not possess;
- publish anything to a live channel;
- alter the content of a hall entry.
---
## 15. Security Requirements
### Authentication
Consumers authenticate to the gateway. The bot token authenticates the adapter
to Telegram and is never a consumer credential.
### Authorization
Only declared cohorts may publish. `telegram-subscribers` is an observed
population, not a caller.
### Tenant isolation
Single tenant. The interface serves HelixForge and no one else.
### Data handling
Telegram user identity (Canon ID-01) must not enter the evidence store in any
form. Subscriber engagement is recorded only as aggregate counts, subject to the
cohort minimum size. This is a hard constraint: readers of a public channel have
not consented to individual observation, and a framework built to learn from
consumers must not quietly learn about audiences.
### Logging and provenance
Every publication records which entry, which revision, which message, and which
consumer asked. The bot token appears in no log, error, telemetry event or
evidence record.
### AI boundary
AI-generated changes must pass deterministic security policy, tests,
authorization validation and release controls. At FLUID-2 no AI-generated change
reaches this interface at all.
---
## 16. Quality Requirements
| Quality attribute | Requirement | Hard guardrail? |
|---|---|---:|
| Correctness | No published entry loses content or attribution | yes |
| Idempotency | Publishing an entry twice yields one publication | yes |
| Availability | Best effort; publication may be retried | no |
| Reliability | A failed publication reports why and may be retried safely | yes |
| p95 latency | Under 10s excluding Telegram rate limiting | no |
| Error rate | Under 1% of publication attempts | yes |
| Security | Token never leaves custody | yes |
| Auditability | Every publication traceable to entry, revision and caller | yes |
| Rollback | Any revision can be replaced by its predecessor within one policy generation | yes |
---
## 17. Economic Constraints
### Adaptive compute budget
| Budget | Limit |
|---|---:|
| analysis per day | 1 classification pass |
| hypothesis generation | manual only at FLUID-2 |
| candidate generation | not permitted at FLUID-2 |
| concurrent experiments | 1 |
| autonomous implementation effort | none |
### Cost policy
- Adaptive budget exhaustion must not affect publication.
- The dominant cost here is human attention, not compute. A hypothesis that
needs more review time than the improvement is worth should be deferred.
---
## 18. Backend Capability Escalation
### Preferred requirement destination
Telegram platform constraints cannot be escalated to anyone — there is no
recipient. A requirement naming a Telegram limitation is recorded as evidence
for **boundary learning** instead, and the interface adapts around the
constraint or declares the demand out of scope.
Requirements against `hall-of-helix` (entry structure, metadata) go to that
repository as an issue referencing the originating hypothesis.
### Required response states
ACCEPTED, PLANNED, AVAILABLE, PARTIALLY_AVAILABLE, OUT_OF_SCOPE, REJECTED,
SUPERSEDED.
### Boundary-learning policy
Repeated OUT_OF_SCOPE or REJECTED responses trigger a review of whether the
demand belongs to a neighbouring interface — most likely `fluid-x` or
`fluid-substack`, which face the same publishing problem under different
platform constraints.
---
## 19. Audit and Provenance Policy
For every promoted change the system must retain enough evidence to reconstruct
originating pressure, hypothesis, candidate, verification, approval, deployment,
affected cohorts, measured outcome and final disposition.
### Retention period
Evidence: 365 days. Publication records: indefinite, because they are the record
of what was said in HelixForge's name.
### Immutable evidence store
The fluid-core evidence store, append-only at the database.
---
## 20. Review and Amendment
### Amendment authority
Bernd Worsch, as the owner of `helix-forge`. An amendment is a governance change
and is recorded as a new intent version; the previous version stays retrievable
because revisions published under it were governed by it.
### Review cadence
On any of: raising the authority mode, adding a capability family currently out
of scope, the first 100 subscribers, or annually.
### Last reviewed
2026-09-04 (initial)
---
## 21. Summary
The Daimon should optimize aggressively inside this document and remain
conservative at its boundaries.
This interface may evolve in how it presents an entry. It may not evolve in
whose voice it speaks.

View file

@ -1,3 +1,13 @@
# fluid-telegram
Fluid interface for telegram messaging and interaction platform.
Fluid interface for telegram messaging and interaction platform.
## Status
The FLUID interface `helix-forge-telegram-publishing` is prepared but not yet
live. Governance, contract, seed hypotheses and the adapter's connector contract
are in place; the bot, the channel and the adapter itself are not.
- Governance: [`InterfaceEvolutionIntent.md`](InterfaceEvolutionIntent.md)
- Work: [`workplans/FT-WP-0001-telegram-identity-and-hall-channel.md`](workplans/FT-WP-0001-telegram-identity-and-hall-channel.md)
- Handover from fluid-core: `fluid-core/docs/handover/fluid-telegram.md`

1536
TelegramCapabilityCanon.md Executable file

File diff suppressed because it is too large Load diff

103
contracts/r1.openapi.yaml Normal file
View file

@ -0,0 +1,103 @@
openapi: "3.1.0"
info:
title: HelixForge Telegram Publishing
version: "R-1"
description: |
Publishes hall-of-helix entries to a Telegram channel.
The interface takes an entry as it was written and is responsible for
everything about how it becomes a Telegram post. A caller should never need
to know Telegram's constraints.
Publication is idempotent on entry_id: publishing the same entry twice
yields one publication. This matters because the caller is usually
automation that may be retried.
paths:
/v1/hall-entries:
post:
operationId: publishEntry
summary: Publish a hall-of-helix entry
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [entry_id, title, body]
additionalProperties: false
properties:
entry_id:
type: string
minLength: 1
maxLength: 128
description: |
The hall entry's own id, from its frontmatter. Publication
is idempotent on this value.
title:
type: string
minLength: 1
maxLength: 256
body:
type: string
minLength: 1
description: |
The entry body as Markdown, frontmatter removed.
Deliberately unbounded. Telegram's 4096-character message
limit is this interface's problem, not the caller's, and a
maxLength here would push it back onto them.
display_name:
type: string
maxLength: 128
worker_kind:
type: string
enum: [human, agent-session]
recorded_at:
type: string
format: date
source_url:
type: string
description: Canonical location of the entry in hall-of-helix.
responses:
"201":
description: Published
"200":
description: Already published; the existing publication is returned
"400":
description: The entry cannot be published faithfully
"502":
description: Telegram was unreachable or refused the message
get:
operationId: listPublications
summary: List publications, newest first
parameters:
- name: limit
in: query
required: false
schema: {type: integer, minimum: 1, maximum: 100}
- name: state
in: query
required: false
schema:
type: string
enum: [pending, published, failed, superseded]
responses:
"200":
description: Publications
/v1/hall-entries/{entry_id}:
get:
operationId: getPublication
summary: Publication state for one entry
parameters:
- name: entry_id
in: path
required: true
schema: {type: string, minLength: 1}
responses:
"200":
description: Publication state
"404":
description: This entry has not been published

100
docs/adapter-contract.md Normal file
View file

@ -0,0 +1,100 @@
# Telegram adapter — connector capability contract
The adapter is an ordinary HTTP service. fluid-core sits in front of it and
asks nothing of it beyond the endpoints below, so it may be written in whatever
suits the platform — grammY on Node is the obvious choice for the Telegram Bot
API.
## What the adapter owns
- Rendering a hall entry into Telegram's message forms.
- Talking to the Telegram Bot API.
- Custody of the bot token for the duration of a call.
- Idempotency: recognising an `entry_id` it has already published.
## What the adapter must not do
- Create, rename or delete channels, groups or bots.
- Change membership, administrators or permissions.
- Post to any chat other than the one it is configured with.
- Edit entry content to make it fit. If an entry cannot be published
faithfully, refuse it — that refusal is the pressure signal.
- Log, echo or forward the bot token, including in error messages.
## Endpoints
Exactly the three operations in `contracts/r1.openapi.yaml`, at the same paths.
The gateway validates requests against that contract before the adapter sees
them, so the adapter may assume shape but must still assume hostile content.
## Backend capability contract
| Property | Value |
|---|---|
| Backend identity | Telegram Bot API (`api.telegram.org`) |
| Authentication | Bot token, injected at start from OpenBao; never in the image |
| Timeout | 30s per Telegram call |
| Retry | On 429 only, honouring `retry_after`. Never retry a `sendMessage` that may have succeeded |
| Circuit breaker | 5 consecutive failures opens for 60s |
| Rate limits | ~20 messages/minute to a channel; ~30 requests/second overall |
| Error mapping | 429 → 502 with `retry_after` preserved; 400 → 400; network failure → 502 |
| Tenant propagation | None; single tenant |
## The idempotency requirement
`POST /v1/hall-entries` is idempotent on `entry_id`. The caller is automation
that may be retried, and a duplicate hall entry in the channel is not something
you can quietly undo — subscribers have already seen it.
The adapter keeps its own mapping from `entry_id` to the Telegram message ids it
produced. On a repeat call it returns `200` with the existing publication rather
than posting again. If the entry content has changed, it edits the existing
messages rather than posting new ones.
This state is the adapter's, not fluid-core's. The evidence store records that a
publication happened; the adapter records which Telegram messages implement it.
## Rendering rules
Telegram formatting is not Markdown. Entry Markdown must be converted, not
passed through.
| Entry construct | Telegram |
|---|---|
| `# Heading` | Bold line |
| `**bold**`, `_italic_` | `<b>`, `<i>` in HTML parse mode |
| Fenced code | `<pre>` |
| Tables | Flattened to lines; Telegram has no table |
| Links | `<a href>` |
| Frontmatter | Stripped; `display_name` and `recorded_at` surfaced in the header |
Anything the converter cannot represent faithfully must fail the publication
rather than degrade it silently. `entry_content_loss` is a hard guardrail in
both competing hypotheses, and it can only be honest if the adapter refuses
rather than approximates.
## Telemetry the adapter should emit
The gateway observes the request path already. The adapter adds what only it can
see, by POSTing to the control plane's `/control/v1/telemetry`:
- `messages_per_entry` — how many Telegram messages one entry became.
- `entry_content_loss` — whether the conversion dropped anything.
- Telegram rate-limit encounters, as error events with class `backend_failure`.
It must not emit subscriber identity in any form. Engagement metrics
(`read_through_rate`, `reaction_rate`) come from Telegram's channel statistics
as aggregates, never per-reader.
## Configuration
| Variable | Meaning |
|---|---|
| `TELEGRAM_BOT_TOKEN` | Injected from OpenBao at start |
| `TELEGRAM_CHANNEL_ID` | The channel this adapter publishes to |
| `FLUID_CONTROL_URL` | Where to POST telemetry |
| `HALL_BASE_URL` | Base URL for links back to hall-of-helix |
One adapter instance serves one channel. The private test channel and the public
channel are separate instances with separate configuration, which is what keeps
a test publication from reaching subscribers by accident.

70
docs/observation.md Normal file
View file

@ -0,0 +1,70 @@
# Cohorts, telemetry and redaction
## Cohorts
Four populations, deliberately coarse. Blueprint §6.3 asks for cohorts stable
enough to compare over time and no more granular than the analysis requires.
| Cohort | Assigned by | Why it is distinct |
|---|---|---|
| `hall-publishing-jobs` | `X-FLUID-Consumer` prefix `hall-` | Batch behaviour; publishes many entries at once |
| `helix-forge-agents` | `X-FLUID-Consumer` prefix `agent-` | One entry at a time, at session end; the richest pressure signal |
| `operators` | `X-FLUID-Consumer` prefix `op-` | Rare, deliberate, often corrective |
| `telegram-subscribers` | Not a caller | An observed population, never an API consumer |
`telegram-subscribers` deserves care. It appears in experiment allocation and in
engagement metrics, but it never authenticates and never appears as
`ConsumerRef`. Treating readers as consumers would be the first step toward
observing them individually.
## Redaction
```
CohortMinimumSize: 5
RetentionDays: 365
AllowRawPayload: false
```
Beyond the fluid-core defaults, this interface adds:
- **No Telegram user identity, ever.** Canon ID-01 identities — user ids,
usernames, display names of subscribers — must not reach the evidence store
in any form, redacted or otherwise. There is no legitimate analysis that
needs them, so the safe rule is that they never arrive.
- **Entry content is not telemetry.** An entry body may appear in a request; it
must not appear in a telemetry event. What is recorded is its length, its
section count, and whether conversion succeeded.
- **The bot token appears nowhere.** Not in errors, not in traces, not in
redacted form.
## The pseudonymization salt
Generate once, store in OpenBao alongside the bot token, inject as
`FLUID_REDACTION_SALT`:
```bash
openssl rand -hex 32
```
It must never change. Rotating it makes every previously-seen consumer look
new, which silently invalidates every longitudinal comparison the interface has
accumulated. If it is ever compromised, the correct response is to accept the
loss and start a fresh evidence baseline, not to rotate quietly.
## Metrics
| Metric | Role | Source |
|---|---|---|
| `read_through_rate` | primary | Telegram channel statistics, aggregate |
| `messages_per_entry` | guardrail | Adapter |
| `entry_content_loss` | guardrail | Adapter |
| `error_rate` | guardrail | Gateway |
| `reaction_rate` | secondary | Telegram channel statistics, aggregate |
| `time_to_first_read` | learning | Telegram channel statistics, aggregate |
`read_through_rate` is the one to be careful about. Telegram reports view counts
per message, not per reader. For the teaser form it is link clicks over views;
for the serialized form it is views of the last part over views of the first.
Those are not the same measurement, and comparing them directly is the weakest
part of the first experiment. Say so in the experiment's amendment record rather
than letting the comparison look cleaner than it is.

76
docs/runbook.md Normal file
View file

@ -0,0 +1,76 @@
# Runbook — helix-forge-telegram-publishing
## Rotating the bot token
1. In BotFather: `/revoke` for the bot, which issues a new token.
2. Write the new token to OpenBao at the interface's secret path.
3. Restart the adapter. It reads the token at start only.
4. Confirm with one publication to the **private test channel**.
The old token stops working the moment it is revoked, so publications in flight
will fail. Do this when nothing is publishing.
## Recovering channel administration
If the bot loses its administrator rights, it cannot post and every publication
fails with a Telegram `400`.
1. In the channel's administrator settings, confirm the bot is present with
**Post Messages** enabled.
2. Grant nothing else. The bot does not need to edit others' messages, delete
messages, or manage members, and the intent forbids it from using such
rights if it had them.
3. If the bot was removed entirely, re-add it and re-run one test publication.
A human must do this. The Daimon is forbidden from changing channel membership
or permissions (`InterfaceEvolutionIntent.md` §7).
## Rolling back a revision
Publish a routing policy with a higher generation naming the previous revision
as default:
```bash
fluid experiment stop <experiment-id> --generation N --default-revision R-1 \
--reason "..." --policy-out rollback.json
fluid policy put --file rollback.json
```
If no experiment is running, write the policy by hand with `default_revision`
set to the known-good revision and a generation above the current one. Rollback
is a document replacement; nothing is unwound.
Already-published Telegram messages are **not** rolled back. They were seen.
Correcting a published entry is an edit through the adapter, not a rollback.
## An entry published wrongly
1. Stop further publication: install a policy pointing at the last known-good
revision.
2. Correct the entry in `hall-of-helix` and re-publish it. The adapter edits the
existing messages rather than posting new ones.
3. If the entry cannot be corrected in place — wrong entry entirely, or content
that should not have been published — delete the messages by hand in
Telegram and record why in the publication's audit trail.
4. Record a pressure observation. An entry published wrongly is exactly the
evidence the framework exists to collect, and the instinct to tidy it away
is the instinct to lose it.
## Checking what happened
```bash
export FLUID_INTERFACE=helix-forge-telegram-publishing
fluid audit trace R-1 # how a revision came to exist, and what followed
fluid pressure list # what the interface is currently struggling with
fluid telemetry --limit 50 # recent traffic
fluid events --entity <entry-id> # one publication's history
```
## When the evidence store is unavailable
Publication continues. The gateway serves from cached configuration and buffers
telemetry, dropping it if the buffer fills. You lose observation, not service.
Do not promote anything while the store is down: the promotion would be
unauditable, which is the one thing this framework will not trade away.

View file

@ -0,0 +1,68 @@
# The first live experiment: which presentation serves readers better.
#
# Do not start this until the private test channel has confirmed both forms
# render correctly, and the public channel has enough subscribers for the
# result to mean anything. The intent (section 12) is explicit that an
# underpowered window should report INCONCLUSIVE rather than be shortened.
#
# fluid experiment design --file experiments/E-tg-length.yaml
# fluid experiment start E-tg-length --generation 2 \
# --default-revision R-2 --policy-out rp.json
# fluid policy put --file rp.json
fluid_experiment:
schema_version: "0.1"
id: "E-tg-length"
interface_id: "helix-forge-telegram-publishing"
hypothesis_refs:
- "H-tg-teaser"
- "H-tg-serial"
# R-2 implements the teaser form, R-3 the serialized form. Neither is the
# control in the usual sense: R-1 cannot publish a long entry at all, so
# this is a comparison between two answers rather than against a baseline.
control_revision: "R-2"
candidate_revisions:
- "R-3"
cohorts:
- "telegram-subscribers"
allocation:
control: 0.5
candidate: 0.5
# An even split because there is no incumbent to protect. The usual 90/10
# caution exists to limit exposure to an unproven candidate; here both arms
# are equally unproven and the scarce resource is entries, not safety.
metrics:
primary:
- "read_through_rate"
guardrails:
- "entry_content_loss"
- "error_rate"
- "messages_per_entry"
secondary:
- "reaction_rate"
- "forward_rate"
learning:
- "time_to_first_read"
start_conditions:
- "both revisions verified against the private test channel"
- "channel has at least 100 subscribers"
stop_conditions:
- "hard_guardrail_violation"
- "entry_content_loss"
- "manual_stop"
- "max_duration_reached"
max_duration_hours: 2160 # 90 days: roughly 12 entries at current cadence
result:
state: "PLANNED"
preferred_revision: null
evidence_refs: []

125
hypotheses/H-tg-serial.yaml Normal file
View file

@ -0,0 +1,125 @@
# Competing hypothesis B, in group CG-tg-length.
fluid_hypothesis:
schema_version: "0.1"
id: "H-tg-serial"
interface_id: "helix-forge-telegram-publishing"
state: "DRAFT"
title: "The whole entry in the channel serves readers better than a link"
observation:
summary: >
85% of hall entries exceed Telegram's 4096-character message limit, with
a median body of 5767 characters. The interface must choose a form for
long entries before it can publish at all.
evidence_refs:
- "measurement:hall-of-helix/entries@2026-09-04"
- "pressure:P-tg-length"
affected_cohorts:
- "telegram-subscribers"
pressure:
classes:
- "missing_interface_capability"
pressure_refs:
- "P-tg-length"
severity: 0.85
confidence: 1.0
explanation:
claim: >
A link out of Telegram is a cost most readers will not pay. Telegram's
rich-text posts are built for long-form reading and the audience is
already there. Serializing the entry across a small number of linked
messages keeps the whole account where the reader is, and the median
entry needs only two.
reach:
score: 0.5
explains:
- "P-tg-length"
notes: >
Medium reach. It answers the length constraint natively but does not
transfer to platforms with harder limits.
proposed_adaptation:
class: "presentation"
summary: >
Split the entry at section boundaries into a small number of rich-text
messages, each linked to the previous, with the visual attached to the
first.
implementation_scope: "interface_only"
backend_requirements:
required: false
requirement_refs: []
expected_outcomes:
- metric: "read_through_rate"
cohort: "telegram-subscribers"
target: 0.15
direction: "higher"
- metric: "messages_per_entry"
cohort: "all"
baseline: 1.0
target: 2.0
direction: "higher"
guardrails:
- metric: "entry_content_loss"
operator: "=="
threshold: false
- metric: "error_rate"
operator: "<="
threshold: 0.01
- metric: "messages_per_entry"
operator: "<="
threshold: 4
fitness_dimensions:
expected:
client_utility: 0.5
discoverability: 0.3
simplicity: -0.2
correctness: 0.0
security: 0.0
complexity:
expected_delta:
operation_count: 0
concept_count: 1
alternative_path_count: 1
cognitive_load: 0.2
score: 0.3
risk:
level: "LOW"
reasons:
- "presentation only"
- "splitting can lose meaning at a bad boundary; guarded by content-loss check"
economics:
estimated_experiment_cost: 0.0
estimated_implementation_cost: 0.0
currency: "EUR"
expected_value_class: "MEDIUM"
learning_value:
score: 0.8
notes: >
Tests whether an audience will read long-form in place, which is the
question behind every publishing interface HelixForge will build.
success_criteria:
expression: >
read_through_rate >= 0.15 AND messages_per_entry <= 4 AND no guardrail violation
failure_criteria:
expression: >
any hard guardrail violation
OR read_through_rate below the teaser alternative over the same window
outcome:
status: null
summary: null
evidence_refs: []

125
hypotheses/H-tg-teaser.yaml Normal file
View file

@ -0,0 +1,125 @@
# Competing hypothesis A, in group CG-tg-length.
#
# Create with:
# fluid hypothesis create --file hypotheses/H-tg-teaser.yaml
# fluid hypothesis compete --group CG-tg-length H-tg-teaser H-tg-serial
fluid_hypothesis:
schema_version: "0.1"
id: "H-tg-teaser"
interface_id: "helix-forge-telegram-publishing"
state: "DRAFT"
title: "A teaser and a link serve readers better than the whole entry"
observation:
summary: >
85% of hall entries exceed Telegram's 4096-character message limit, with
a median body of 5767 characters. The interface must choose a form for
long entries before it can publish at all.
evidence_refs:
- "measurement:hall-of-helix/entries@2026-09-04"
- "pressure:P-tg-length"
affected_cohorts:
- "telegram-subscribers"
pressure:
classes:
- "missing_interface_capability"
pressure_refs:
- "P-tg-length"
severity: 0.85
confidence: 1.0
explanation:
claim: >
A channel post is scanned, not read. A hall entry is a considered
first-person account that rewards attention. Presenting an opening and a
link lets a reader decide to give it that attention somewhere designed
for reading, rather than committing to a wall of text in a feed.
reach:
score: 0.5
explains:
- "P-tg-length"
notes: >
Medium reach. It answers the length constraint and would answer the
same constraint on X, but it does not address entries with visuals or
entries readers might want to keep in the channel.
proposed_adaptation:
class: "presentation"
summary: >
Publish the title, the display name, the opening of the entry, and a link
to the full entry in hall-of-helix.
implementation_scope: "interface_only"
backend_requirements:
required: false
requirement_refs: []
expected_outcomes:
- metric: "read_through_rate"
cohort: "telegram-subscribers"
target: 0.15
direction: "higher"
- metric: "messages_per_entry"
cohort: "all"
baseline: 2.0
target: 1.0
direction: "lower"
guardrails:
- metric: "entry_content_loss"
operator: "=="
threshold: false
- metric: "error_rate"
operator: "<="
threshold: 0.01
fitness_dimensions:
expected:
client_utility: 0.4
discoverability: 0.5
simplicity: 0.6
correctness: 0.0
security: 0.0
complexity:
expected_delta:
operation_count: 0
concept_count: 0
cognitive_load: -0.1
score: 0.05
risk:
level: "LOW"
reasons:
- "presentation only; no contract or authorization change"
- "fully reversible by replacing the revision"
economics:
estimated_experiment_cost: 0.0
estimated_implementation_cost: 0.0
currency: "EUR"
expected_value_class: "MEDIUM"
learning_value:
score: 0.8
notes: >
Whichever way this resolves, the answer transfers directly to
fluid-x and fluid-substack, which face the same problem under
different constraints.
success_criteria:
expression: >
read_through_rate >= 0.15 AND no guardrail violation
failure_criteria:
expression: >
any hard guardrail violation
OR read_through_rate below the serialized alternative over the same window
outcome:
status: null
summary: null
evidence_refs: []

View file

@ -0,0 +1,47 @@
# Seed pressure record.
#
# This is not derived from telemetry. It is a structural constraint measured
# directly against hall-of-helix on 2026-09-04, and it is real before the
# interface has served a single request. The intent (section 13) permits a
# structural constraint to found a hypothesis without waiting for repetition.
#
# Record it with:
# fluid pressure ... (or let the first classification pass confirm it)
fluid_pressure:
schema_version: "0.1"
id: "P-tg-length"
interface_id: "helix-forge-telegram-publishing"
class: "missing_interface_capability"
first_seen: "2026-09-04T00:00:00Z"
last_seen: "2026-09-04T00:00:00Z"
affected_cohorts:
- "hall-publishing-jobs"
- "helix-forge-agents"
frequency:
observations: 80
independent_consumers: 2
severity: 0.85
confidence: 1.0
summary: >
Telegram limits a single message to 4096 characters. Of the 94 entries in
hall-of-helix on 2026-09-04, 80 have bodies exceeding that limit — 85%.
The median body is 5767 characters and the largest is 10220. No entry of
typical length can be published as one message, so the interface cannot
fulfil its mission without deciding how a long entry becomes a post.
evidence_refs:
- "measurement:hall-of-helix/entries@2026-09-04"
- "canon:fluid-telegram/TelegramCapabilityCanon.md#PUB-02"
status: "ANALYZING"
linked_hypotheses:
- "H-tg-teaser"
- "H-tg-serial"

52
revisions/R-1.yaml Normal file
View file

@ -0,0 +1,52 @@
# Revision descriptor for R-1.
#
# Fill in the digest and upstream, then publish through the pipeline:
#
# fluid revision publish --file revisions/R-1.yaml \
# --key-file "$FLUID_SIGNING_KEY" \
# --adaptation-classes presentation \
# --approved-by "$USER" --traffic-share 1.0
#
# The digest is the sha256 of contracts/r1.openapi.yaml:
# printf 'sha256:%s' "$(sha256sum contracts/r1.openapi.yaml | cut -d' ' -f1)"
revision:
schema_version: "0.1"
id: "R-1"
interface: "helix-forge-telegram-publishing"
state: "stable"
contract:
type: "openapi"
digest: "sha256:<digest of contracts/r1.openapi.yaml>"
source: "contracts/r1.openapi.yaml"
runtime:
upstream: "http://fluid-telegram-adapter:8080"
# Telegram rate-limits channel posts to roughly 20 per minute. A generous
# timeout is correct here: publication is not latency-sensitive, and
# failing fast would turn a rate limit into a lost entry.
timeout_ms: 30000
retry:
# Only GET is retried by the connector; POST /v1/hall-entries is
# idempotent on entry_id, but the retry decision stays with the adapter,
# which knows whether Telegram accepted the message.
max_attempts: 1
circuit_breaker:
failure_threshold: 5
reset_after_ms: 60000
intent:
version: "IEI-1"
routing:
eligible_cohorts:
- "hall-publishing-jobs"
- "helix-forge-agents"
- "operators"
max_traffic_share: 1.0
policy:
compatibility: "additive"
security_check: "passed"
policy_check: "passed"
rollback_to: null

View file

@ -0,0 +1,190 @@
---
id: FT-WP-0001
type: workplan
title: "Establish HelixForge's Telegram identity and publish the Hall of Helix"
domain: infotech
repo: fluid-telegram
status: active
owner: worsch
topic_slug: fluid-telegram
created: "2026-09-04"
updated: "2026-09-04"
planning_priority: high
planning_order: 1
depends_on:
- FLUID-WP-0008
related_repos:
- fluid-core
- helix-forge
- hall-of-helix
---
# FT-WP-0001 — Telegram identity and the Hall of Helix channel
Establish a Telegram identity for HelixForge and a channel publishing
hall-of-helix entries, as the first real FLUID interface.
Everything that could be prepared without credentials has been: the governing
intent, the R-1 contract, the adapter's connector contract, the observation
configuration, the seed pressure record, and two competing hypotheses for the
first experiment. See `docs/` and the handover package at
`fluid-core/docs/handover/fluid-telegram.md`.
What remains needs either a person with a Telegram account or a running
adapter.
## T01 — Register the bot
```task
id: FT-WP-0001-T01
status: todo
priority: high
```
**Human step.** Register the HelixForge bot through BotFather (Canon BOT-01).
Bots are first-class Telegram accounts and need no phone number.
Set the bot's name, description and profile image to something a reader
encountering it cold would recognise as HelixForge.
Write the token to OpenBao immediately; do not paste it anywhere else, not even
briefly. Consistent with `HF-WP-0002` (bao.coulomb.social).
## T02 — Create the channels
```task
id: FT-WP-0001-T02
status: todo
priority: high
```
**Human step.** Create the **private test channel first**, then the public
channel (Canon PUB-01). Add the bot as administrator with **Post Messages**
only — the intent forbids it from using any wider right, so granting one would
be a permission the system is not allowed to exercise.
Every verification below runs against the private channel. Nothing reaches the
public channel until the rendering has been seen by a person.
## T03 — Generate and store the redaction salt
```task
id: FT-WP-0001-T03
status: todo
priority: high
```
`openssl rand -hex 32`, stored in OpenBao beside the token, injected as
`FLUID_REDACTION_SALT`. It must never change: see `docs/observation.md` for why
rotating it silently invalidates every longitudinal comparison.
## T04 — Record the intent and publish R-1
```task
id: FT-WP-0001-T04
status: todo
priority: high
```
```bash
export FLUID_INTERFACE=helix-forge-telegram-publishing
fluid intent put --version IEI-1 --file InterfaceEvolutionIntent.md --activate
# fill the digest in revisions/R-1.yaml, then
fluid revision publish --file revisions/R-1.yaml --key-file "$FLUID_SIGNING_KEY" \
--adaptation-classes presentation --approved-by "$USER" --traffic-share 1.0
```
Generate a real signing key pair for this; `--ephemeral-key` is development
only, and a revision signed with one stops verifying after a restart.
## T05 — Build the adapter
```task
id: FT-WP-0001-T05
status: todo
priority: high
```
Implement the three operations in `contracts/r1.openapi.yaml` against the
connector contract in `docs/adapter-contract.md`. TypeScript with grammY is the
natural choice; the wire contract makes the language irrelevant to fluid-core.
The two requirements that carry the most weight: publication is idempotent on
`entry_id`, and an entry that cannot be converted faithfully is refused rather
than approximated. `entry_content_loss` is a hard guardrail in both competing
hypotheses and can only be honest if the adapter refuses.
## T06 — Publish one entry to the private channel
```task
id: FT-WP-0001-T06
status: todo
priority: high
```
The first real test. Publish a single hall entry to the private channel and
confirm, by looking at it:
- the rendering is faithful and readable;
- re-publishing the same entry edits rather than duplicates;
- the evidence store contains no Telegram user identity and no bot token;
- `fluid audit trace R-1` reconstructs the publication.
## T07 — Implement both presentation forms as R-2 and R-3
```task
id: FT-WP-0001-T07
status: todo
priority: medium
```
R-2 implements `H-tg-teaser`, R-3 implements `H-tg-serial`. Both are
presentation adaptations over the same contract, so neither needs a contract
change. Verify both against the private channel before either sees a subscriber.
## T08 — Open the public channel and publish the backlog
```task
id: FT-WP-0001-T08
status: todo
priority: medium
```
Point the production adapter at the public channel and publish the existing
entries. 94 entries at once would be both a rate-limit problem and an unkind
introduction for a new subscriber; publish them oldest-first at a human pace.
Whichever of R-2 or R-3 is used here becomes the de facto control for the
experiment, which is why `E-tg-length` names R-2 as control rather than R-1.
## T09 — Run the first experiment
```task
id: FT-WP-0001-T09
status: todo
priority: low
```
Start `E-tg-length` once the channel has roughly 100 subscribers. Before then
the evaluator will report INCONCLUSIVE, and that is the correct answer rather
than a reason to shorten the window.
At one entry per week, a 90-day experiment yields about twelve entries. That is
thin, and the honest expectation is that the first run is inconclusive and
informs a better-powered second one. Record that expectation in the experiment's
amendments rather than discovering it as a disappointment.
## T10 — Close the loop
```task
id: FT-WP-0001-T10
status: todo
priority: low
```
Record the hypothesis outcome, resolve the competition group, and promote the
winner. Then confirm `fluid audit trace` reconstructs the whole chain from the
length-limit pressure through to the promoted revision.
That trace is the deliverable. It is what makes this the first FLUID interface
rather than a bot that posts to a channel.