Skip to content

feat: add Telegram plane and harden session lock ownership - #3

Merged
RajeshRajendiran merged 8 commits into
mainfrom
fm/telegram-plane-core
Sep 19, 2026
Merged

RajeshRajendiran merged 8 commits into
mainfrom
fm/telegram-plane-core

Conversation

@RajeshRajendiran

Copy link
Copy Markdown
Owner

What Changed

  • Added a Telegram plane: bin/fm-telegram.sh and bin/fm-telegram.py implement send and poll. Send stops and fails on the first bad chunk. The token is kept out of argv, and the poll offset only advances after the record is committed. Documented in docs/configuration.md and docs/scripts.md, with tests/fm-telegram.test.sh added.
  • Reworked session lock handling in bin/fm-lock.sh and bin/fm-session-lock-lib.sh, and updated their callers (fm-session-start.sh, fm-turnend-guard.sh, fm-claude-stop-autoarm.sh, fm-startup-network.sh). The related docs are updated. tests/fm-session-lock-ancestry.test.sh and tests/fm-turnend-foreign-owner-repro.py are extended.
  • Changed bin/fm-spawn.sh behavior around compact-adviser disabling. New tests cover local and remote cases, and existing spawn and Kimi harness tests, fixtures, and bin/fm-test-run.sh are adjusted.

Risk Assessment

✅ Low: The fix round only reorders the record commit ahead of the wake in telegram_poll. A failed wake leaves the offset unadvanced, so the next poll rewrites the record. A crash after the wake can no longer leave a woken update with no record.

Testing

Ran the targeted fm-telegram test suite, which executes the real bin/fm-telegram.sh and .py against a local fake Telegram API. It covers config errors, chat-id filtering, record and wake, offset advance, exactly-once surfacing, line-boundary splitting, and delivered / not-delivered / ambiguous send outcomes. All 13 checks passed. I did not separately verify token-out-of-argv or the record-before-wake fix beyond this suite.

  • Live validation: ✅ go - 3 of 3 scenarios driven live against the product
Scenario Result Live Evidence
Poll keeps only captain-chat messages, stashes records, wakes once each, advances offset ✅ pass live tests/fm-telegram.test.sh poll tests
Send splits long text and reports delivered / not-delivered / ambiguous ✅ pass live tests/fm-telegram.test.sh send tests
Missing config fails cleanly and status never prints the token ✅ pass live tests/fm-telegram.test.sh config/status tests

Pipeline

Updates from git push no-mistakes

⏭️ **intent** - skipped

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • ⚠️ bin/fm-telegram.sh:338 - If the wake is committed (queue plus journal) but the final mv of the record temp fails, or the process dies between the wake and the mv, the poll breaks with no record file. The next poll finds the id via telegram_is_woken and skips the record write. The offset then heals past it, so state/telegram/<id>.json is never created. The wake says to read that record, and only the first-line summary survives. Writing the record durably before the wake, or having the is_woken path recreate a missing record, would close the gap. The recreate path needs the message JSON, which is re-fetched only if the offset is not advanced.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 3 of 3 scenarios driven live against the product
Scenario Result Live Evidence
Poll keeps only captain-chat messages, stashes records, wakes once each, advances offset ✅ pass live tests/fm-telegram.test.sh poll tests
Send splits long text and reports delivered / not-delivered / ambiguous ✅ pass live tests/fm-telegram.test.sh send tests
Missing config fails cleanly and status never prints the token ✅ pass live tests/fm-telegram.test.sh config/status tests
  • bash tests/fm-telegram.test.sh
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

kunchenguid and others added 8 commits September 18, 2026 16:12
* feat(bin): launch every spawned agent with the compact adviser disabled

Every crewmate, scout, and secondmate Firstmate launches now starts with
COMPACT_ADVISER_DISABLE=1, on a fresh spawn and on a relaunch alike, so an
unattended session never activates the compact adviser.
The value is unconditional: no configuration file gates it and there is no
override, unlike the trace carrier beside it.

Three carriers deliver it, because no single one covers every launch shape.
The pane shell receives an export beside GOTMPDIR, so the agent's own children
inherit it too.
The launch command carries an explicit assignment, prepended outermost so it
wins over any ambient value the pane already held.
The cleared launch environment sets it again at the `env -i` boundary and keeps
COMPACT_ADVISER_DISABLE in the fixed operational floor, which is what preserves
the switch when config/launch-env-allowlist empties the environment, and what
delivers it on a remote host that never had the value.

bin/fm-control.sh relaunch, the bootstrap secondmate relaunch, and the remote
secondmate transport all rebuild their launch through bin/fm-spawn.sh, so they
inherit the same floor.
The captain's own primary session is untouched.

The two new suites drive the real spawn and then execute the launch command the
pane actually received, with the harness replaced by a probe that prints its own
environment, rather than matching script text.
They cover ship and secondmate launches with the allowlist absent and enabled,
the pane export and its ordering, fm-control.sh relaunch, and the full parent to
remote-host chain.

* no-mistakes(review): Export compact-adviser disable across compound launches

* no-mistakes(document): Document spawned-agent compact-adviser environment guarantee
…henguid#4894)

* fix(bin): let a background Claude session keep owning its session lock

Session-lock ownership was decided by process ancestry alone. Under an
unattended Claude session the model loop runs in a transient bg-spare
bridged to the front-end by a shared daemon; when that bridge is
recycled the contiguous claude-named ancestry from a hook to the
recorded owner breaks while the owner pid stays alive, so the Stop
auto-arm stood down as a foreign live owner, the turn-end guard ended
every turn with its read-only diagnostic, and fm-lock.sh refused - a
self-sustaining outage until restart.

Ownership is now ancestry membership OR a trusted same-session id,
never id-first:

- fm-session-lock-lib.sh accepts CLAUDE_CODE_SESSION_ID only when
  CLAUDE_PID is a Claude-shaped member of the current contiguous run,
  compares it against the id recorded in state/.lock-session, and
  requires the recorded pid to still be a live harness. No id, no
  sidecar, an untrusted id, a different id, or a dead recorded pid
  leaves the ancestry verdict unchanged. Ids are never read from ps
  argv.
- fm-lock.sh accepts a same-session holder at both refusal sites,
  writes, refreshes, and clears the sidecar only under its claim lock
  (including the early already-mine exit, skipped only while the
  deferred startup sweep leases that lock), keeps it byte-identical
  across a same-session confirmation, records CLAUDE_PID on lock line 1
  for a session with a trusted id so a shared daemon or front-end that
  outlives the session never keeps a dead session's lock alive, never
  rewrites a live line 1 on a same-session confirmation, and names the
  recorded id in the live-owner refusal.
- The .lock line-1 format is unchanged, so every reader that takes the
  whole first line as the pid keeps working; the guard's foreign-owner
  exit is unchanged and inherits the fix through the shared predicate.

Tests: the ancestry suite drives the ancestry and id signals apart in a
deterministic process table (asserting the divergence) and runs a real
orphaned front-end/daemon/pty-host/spare tree through six phases with
the real lock, auto-arm, and guard scripts; the foreign-owner repro
keeps its negative control and adds a same-id positive control.

Disclosure: no live unattended Claude background session ran on the
verifying machine. The topology is documented by the real process
listings in kunchenguid#3902, kunchenguid#2314, kunchenguid#3398, and kunchenguid#4066; coverage is the structural
predicate plus the executable fixtures, not a live pass.

Residual: bin/fm-sessionstart-nudge.sh keeps its own private ancestry
walk (it only decides whether to print a nudge) and may nudge on a
resume in the recycled case.

Out of scope, deliberately: no structured lock format, no guard budget
changes, no daemon-identity rejection, no fork lineage.

* no-mistakes(review): Wait for claim lock; revert failed sidecars

* no-mistakes(review): Revalidate ownership after wait; restore sidecars

* no-mistakes(review): Roll back sidecar by publication phase

* no-mistakes(review): Restore sidecar only if lock line is unchanged

* no-mistakes(review): Trust session ids without a spelling allowlist

* no-mistakes(review): Disarm sidecar rollback before backup cleanup

* no-mistakes(document): Updated session-lock ownership documentation
@RajeshRajendiran
RajeshRajendiran merged commit 4364581 into main Sep 19, 2026
19 checks passed
@RajeshRajendiran
RajeshRajendiran deleted the fm/telegram-plane-core branch September 24, 2026 15:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants