Problem
A native omp task subagent can release the parent bridge's bot.lock and replace the parent's dm-owner.json identity during session_shutdown, even though the child skipped bridge startup. The parent's actual poller remains alive.
Parent and child share a PID and the module-scoped lock nonce, but have separate extension closures and Poller instances. The child's stopBot() stops only its own unused poller while releasing the shared parent's lock. The preceding refreshTopicClaim(ctx) also replaces the DM owner's durable session identity because its PID matches.
This leaves a live parent poller without its lock, allowing another session to acquire it, and points durable untopiced-DM ownership at the ended child. This is the unguarded session_shutdown path, not the agent_end path fixed by #65 for #64.
Repro
An isolated offline harness used two real exported telegramExtension() factories and real Poller instances in one process:
- Use a temporary
HOME and OMP_TELEGRAM_STATE_DIR, with enabled test state configured for session poller fallback rather than the standalone daemon. Capture each factory's event handlers separately. Give parent and child distinct session IDs and file paths. The child has hasUI: false and yield in its active tools.
- Intercept global
fetch: return stub responses for getMe and setMyCommands, and leave getUpdates pending until its AbortSignal is aborted. Do not replace the pollers or the filesystem lock functions, and do not contact Telegram.
- Emit the parent's
session_start. It acquires the real temporary poll lock, writes the parent DM-owner identity, and starts a pending getUpdates request.
- Emit the child's
session_start. The existing task-subagent guard skips bridge startup.
- Emit and await the child's
session_shutdown.
Observed after step 5:
| State |
Expected |
Actual |
bot.lock |
Still held by parent |
Removed |
dm-owner.json session identity |
Parent |
Child |
Parent getUpdates signal |
Not aborted |
Not aborted: parent poller is still alive |
As a control, the real parent's shutdown aborted the parent's pending request and left no poll lock. Adding the same early task-subagent guard used at startup to session_shutdown preserved the parent lock and DM owner in an isolated local fix, while normal parent cleanup still worked.
These observations are from the isolated reproduction, not instrumentation of an original live Telegram interruption.
Acceptance criteria
Entry points
All references are against v0.13.0:
Environment
- omp 18.2.4
- omp-telegram 0.13.0 (
v0.13.0, commit 0ce609d7e019f32a7ea734b35aa87dbb1190f990)
- Bun 1.3.13
- Linux x86_64
Problem
A native omp task subagent can release the parent bridge's
bot.lockand replace the parent'sdm-owner.jsonidentity duringsession_shutdown, even though the child skipped bridge startup. The parent's actual poller remains alive.Parent and child share a PID and the module-scoped lock nonce, but have separate extension closures and
Pollerinstances. The child'sstopBot()stops only its own unused poller while releasing the shared parent's lock. The precedingrefreshTopicClaim(ctx)also replaces the DM owner's durable session identity because its PID matches.This leaves a live parent poller without its lock, allowing another session to acquire it, and points durable untopiced-DM ownership at the ended child. This is the unguarded
session_shutdownpath, not theagent_endpath fixed by #65 for #64.Repro
An isolated offline harness used two real exported
telegramExtension()factories and realPollerinstances in one process:HOMEandOMP_TELEGRAM_STATE_DIR, with enabled test state configured for session poller fallback rather than the standalone daemon. Capture each factory's event handlers separately. Give parent and child distinct session IDs and file paths. The child hashasUI: falseandyieldin its active tools.fetch: return stub responses forgetMeandsetMyCommands, and leavegetUpdatespending until itsAbortSignalis aborted. Do not replace the pollers or the filesystem lock functions, and do not contact Telegram.session_start. It acquires the real temporary poll lock, writes the parent DM-owner identity, and starts a pendinggetUpdatesrequest.session_start. The existing task-subagent guard skips bridge startup.session_shutdown.Observed after step 5:
bot.lockdm-owner.jsonsession identitygetUpdatessignalAs a control, the real parent's shutdown aborted the parent's pending request and left no poll lock. Adding the same early task-subagent guard used at startup to
session_shutdownpreserved the parent lock and DM owner in an isolated local fix, while normal parent cleanup still worked.These observations are from the isolated reproduction, not instrumentation of an original live Telegram interruption.
Acceptance criteria
Entry points
All references are against
v0.13.0:session_starthas a task-subagent guard, butsession_shutdowndoes not.stopBot()stops its closure-local poller and then releases the lock.PROCESS_LOCK_NONCEis module-scoped, andreleaseLock()defaults to that nonce andprocess.pid.refreshTopicClaim()refreshes the DM owner on a PID match, using the child context's durable identity.Environment
v0.13.0, commit0ce609d7e019f32a7ea734b35aa87dbb1190f990)