Summary
A scheduled task or watch that holds a persistent Session minted by vibe task/watch add|update --create-session deletes that Session row from the database moments after its first successful visible delivery. The definition keeps its now-dangling run_definitions.session_id, so every subsequent scheduled fire dies at dispatch with agent session id not found: <id> — silently, with last_status=failed, 0s duration and empty result_text.
The delivery-finalization path treats any session anchor whose derived thread_id is None as a provisional channel-level anchor and clears it. It has no way to distinguish a throwaway provisional anchor from a durable --create-session definition anchor, because both satisfy that predicate.
In this environment a single pass of this behaviour orphaned 4 enabled definitions (enabled=1, deleted_at IS NULL, dangling session_id) — 3 have since been repaired, 1 is still outstanding. A further 17 soft-deleted definitions hold the same dangling session_id pattern but will never dispatch again, so they are not active failures. (A naive enabled=1 orphan query without an explicit deleted_at IS NULL filter will overcount, since vibe task remove only sets deleted_at and leaves enabled untouched — worth flagging for anyone writing a similar check.)
Related: #1060 (P2-2 describes watch bookkeeping that does not reflect whether a watch can actually still fire; this issue is one concrete mechanism that produces exactly that state). Not a duplicate of #953 (forever-watch run stampede).
Environment
- avibe-os 3.0.8, installed 2026-07-27 18:22 UTC
- Linux, Python 3.11
- IM surface: Discord, channel scope
- Source root for all line numbers below:
/home/qihan/.local/share/uv/tools/avibe-os/lib/python3.11/site-packages/
- The load-bearing evidence below (the
2026-07-28 12:01:32 clear) was captured on 3.0.8. Earlier events in the same pattern (the 07-27 10:01:15 clear, and the control-case Session created 2026-07-25T03:19:40) ran on the preceding release; its exact version is no longer recoverable (the dist-info was overwritten by the upgrade). What is known is that the 2026-07-27 18:22 upgrade moved the alembic revision from 20260716_0030 to 20260727_0038. This indicates the behavior spans at least two releases, not just 3.0.8.
Root cause
1. clear_provisional_source is decided solely by thread_id is None
core/scheduled_tasks.py:3451, inside _build_delivery_alias_strategy:
clear_provisional_source = session_target.thread_id is None and self._supports_threaded_delivery(session_target)
2. A durable --create-session anchor always derives thread_id = None
core/message_context.py:62-83, tail of thread_id_from_session_anchor():
legacy_prefix = f"{platform}_"
if not base_anchor.startswith(legacy_prefix):
return None
thread_id = base_anchor[len(legacy_prefix):]
return thread_id if thread_id and thread_id != str(channel_id) else None
Anchors minted by --create-session have the form discord_<channel_id>:definition_<uuid4hex12>, generated by _session_anchor_with_suffix at vibe/cli.py:4115-4116. The base segment (split(":", 1)[0]) is exactly discord_<channel_id>, so the derived thread_id equals str(channel_id) and the function returns None. Condition (1) is therefore satisfied for every durable definition anchor.
3. On Discord the second conjunct is always true
modules/im/discord.py:187-188 — should_use_thread_for_reply() is unconditionally True in a channel context, so _supports_threaded_delivery is always True. clear_provisional_source is therefore True for every --create-session definition Session on Discord.
4. The clear fires only when a visible message was delivered
core/message_dispatcher.py:1882-1884, the sole call site of finalize_scheduled_delivery:
if scheduled_anchor_message_id and mutates_turn_lifecycle:
self.controller.session_handler.finalize_scheduled_delivery(context, scheduled_anchor_message_id)
5. Finalization clears the source base
core/handlers/session_handler.py:455-483 (finalize_scheduled_delivery) reads clear_source and delegates to alias_session_base; core/handlers/session_handler.py:450-452:
if clear_source and source_base_session_id != alias_base_session_id:
cleared = self.sessions.clear_session_base(resolved_source_key, source_base_session_id)
6. The clear is a hard DELETE
modules/sessions_facade.py:296 → config/v2_sessions.py:491-493 → storage/sessions_service.py:495-518 delete_agent_sessions() → agent_sessions.delete().
This is a hard delete — not an archive, not a soft delete.
The defect: clear_provisional_source intends to clean up temporary channel-level anchors, but its only discriminator is thread_id is None, which cannot separate a temporary anchor from a durable definition anchor minted by --create-session. Both match.
Second defect that amplifies it: no reverse cleanup, no FK
run_definitions.session_id has no foreign key constraint — storage/models.py:180 is a plain Column("session_id", String, nullable=True) — and the hard-delete path performs no reverse cleanup. The definition is left holding a dangling id until the next scheduled fire, which then fails silently: last_status=failed, 0s runtime, empty result_text, no alert.
Notably, the same invariant is maintained on the UI deletion path: storage/workbench_sessions_service.py:904 archive_session does clean up referencing definitions, and its comment states the reason explicitly ("Deleting — not pausing — is deliberate: a paused definition could be re-enabled later and would then target a dead session"). So the invariant is enforced on one path and omitted on the other.
Evidence (logs)
Session sesyj3b7rvwdr, anchor discord_1494713026509668483:definition_78fc24cbfb20, used by a weekly scheduled task:
2026-07-28 12:00:01 Codex turn started: session=discord_1494713026509668483:definition_78fc24cbfb20
2026-07-28 12:01:31 (task posts its visible output to Discord)
2026-07-28 12:01:32 clear_session_base() - Cleared session base for discord::1494713026509668483:
discord_1494713026509668483:definition_78fc24cbfb20 (1 keys)
The task ran successfully and delivered; 91 seconds later its own Session was deleted.
The logs contain 10 such clear events against definition_* anchors, each matching the last-activity timestamp of one of 5 known-dead Sessions. Example: a clear at 07-27 10:01:15 corresponds to ses7hejj3hraw, whose task failed the next day at 10:00 with agent session id not found: ses7hejj3hraw.
Net effect in this environment: 4 enabled definitions orphaned (3 since repaired, 1 still outstanding), plus 17 soft-deleted definitions with the same dangling session_id that will never dispatch again.
Control case (pins the trigger to visible delivery)
Three definitions minted in the same batch, in the same scope, with the same anchor format, had different fates:
- Two tasks that deliver a visible message → Session deleted.
- One task,
ab2dd2e16504, whose prompt instructs it to reply only with <silent>...</silent> → never produces a delivery message → scheduled_anchor_message_id is None → finalize_scheduled_delivery is never called → its Session sesgrd9mx7yu7 (anchor discord_1494713026509668483:definition_8339036c4bba, created 2026-07-25T03:19:40) has run successfully for several consecutive days and is still alive.
This isolates the trigger to "did the run emit a visible delivery message", and rules out time-based, quota-based, and GC-based explanations.
Reproduction
- In a Discord channel scope, mint a persistent Session for a scheduled task:
vibe task add|update --create-session --scope-id discord::channel::<id> ...
- Confirm the row exists in
agent_sessions and that run_definitions.session_id points at it.
- Let the task fire once, producing a visible message as output (not
<silent>).
- Immediately after delivery, query
agent_sessions — the row is gone.
- Wait for the next scheduled fire — it fails with
agent session id not found, last_status=failed, 0s duration, empty result_text.
Expected behavior
clear_provisional_source should apply only to genuinely one-shot provisional anchors, and a hard delete of a Session should not be able to leave a live definition pointing at a dead id.
Possible directions (suggestions only — the maintainers should pick the right one):
- In
_build_delivery_alias_strategy, exclude anchors whose session_anchor contains :definition_.
- Or invert the check against
run_definitions.session_id: do not clear a Session that any enabled definition still references.
- Independently: add reverse cleanup for
run_definitions.session_id on the hard-delete path (or at minimum a startup self-check), bringing it in line with archive_session.
Impact
Every user who combines --create-session with a scheduled task or watch in a Discord channel scope — and, by the same code path, any IM platform whose should_use_thread_for_reply() returns True. For any such task that pushes a visible message, Session deletion after the first delivery is deterministic, not intermittent.
The only working configuration currently is session_policy=create_per_run with run_definitions.session_id IS NULL, i.e. the definition holds no Session reference at all — which forfeits cross-run conversational memory.
Ruled out
- Automatic GC / retention. A whole-package search found no session TTL /
max_age / keep_last policy; the only retention constant applies to on-disk backup files.
- Schema migration. The 3.0.8 migration scripts only add columns and create indexes on
agent_sessions.
- A second
--create-session call deleting the first. That path is insert-only, and anchors carry a uuid4 suffix so they cannot collide. The vibe session command group has no delete subcommand.
Summary
A scheduled task or watch that holds a persistent Session minted by
vibe task/watch add|update --create-sessiondeletes that Session row from the database moments after its first successful visible delivery. The definition keeps its now-danglingrun_definitions.session_id, so every subsequent scheduled fire dies at dispatch withagent session id not found: <id>— silently, withlast_status=failed, 0s duration and emptyresult_text.The delivery-finalization path treats any session anchor whose derived
thread_idisNoneas a provisional channel-level anchor and clears it. It has no way to distinguish a throwaway provisional anchor from a durable--create-sessiondefinition anchor, because both satisfy that predicate.In this environment a single pass of this behaviour orphaned 4 enabled definitions (
enabled=1,deleted_at IS NULL, danglingsession_id) — 3 have since been repaired, 1 is still outstanding. A further 17 soft-deleted definitions hold the same danglingsession_idpattern but will never dispatch again, so they are not active failures. (A naiveenabled=1orphan query without an explicitdeleted_at IS NULLfilter will overcount, sincevibe task removeonly setsdeleted_atand leavesenableduntouched — worth flagging for anyone writing a similar check.)Related: #1060 (P2-2 describes watch bookkeeping that does not reflect whether a watch can actually still fire; this issue is one concrete mechanism that produces exactly that state). Not a duplicate of #953 (forever-watch run stampede).
Environment
/home/qihan/.local/share/uv/tools/avibe-os/lib/python3.11/site-packages/2026-07-28 12:01:32clear) was captured on 3.0.8. Earlier events in the same pattern (the07-27 10:01:15clear, and the control-case Session created2026-07-25T03:19:40) ran on the preceding release; its exact version is no longer recoverable (the dist-info was overwritten by the upgrade). What is known is that the 2026-07-27 18:22 upgrade moved the alembic revision from20260716_0030to20260727_0038. This indicates the behavior spans at least two releases, not just 3.0.8.Root cause
1.
clear_provisional_sourceis decided solely bythread_id is Nonecore/scheduled_tasks.py:3451, inside_build_delivery_alias_strategy:2. A durable
--create-sessionanchor always derivesthread_id = Nonecore/message_context.py:62-83, tail ofthread_id_from_session_anchor():Anchors minted by
--create-sessionhave the formdiscord_<channel_id>:definition_<uuid4hex12>, generated by_session_anchor_with_suffixatvibe/cli.py:4115-4116. The base segment (split(":", 1)[0]) is exactlydiscord_<channel_id>, so the derivedthread_idequalsstr(channel_id)and the function returnsNone. Condition (1) is therefore satisfied for every durable definition anchor.3. On Discord the second conjunct is always true
modules/im/discord.py:187-188—should_use_thread_for_reply()is unconditionallyTruein a channel context, so_supports_threaded_deliveryis alwaysTrue.clear_provisional_sourceis thereforeTruefor every--create-sessiondefinition Session on Discord.4. The clear fires only when a visible message was delivered
core/message_dispatcher.py:1882-1884, the sole call site offinalize_scheduled_delivery:5. Finalization clears the source base
core/handlers/session_handler.py:455-483(finalize_scheduled_delivery) readsclear_sourceand delegates toalias_session_base;core/handlers/session_handler.py:450-452:6. The clear is a hard DELETE
modules/sessions_facade.py:296→config/v2_sessions.py:491-493→storage/sessions_service.py:495-518delete_agent_sessions()→agent_sessions.delete().This is a hard delete — not an archive, not a soft delete.
The defect:
clear_provisional_sourceintends to clean up temporary channel-level anchors, but its only discriminator isthread_id is None, which cannot separate a temporary anchor from a durable definition anchor minted by--create-session. Both match.Second defect that amplifies it: no reverse cleanup, no FK
run_definitions.session_idhas no foreign key constraint —storage/models.py:180is a plainColumn("session_id", String, nullable=True)— and the hard-delete path performs no reverse cleanup. The definition is left holding a dangling id until the next scheduled fire, which then fails silently:last_status=failed, 0s runtime, emptyresult_text, no alert.Notably, the same invariant is maintained on the UI deletion path:
storage/workbench_sessions_service.py:904archive_sessiondoes clean up referencing definitions, and its comment states the reason explicitly ("Deleting — not pausing — is deliberate: a paused definition could be re-enabled later and would then target a dead session"). So the invariant is enforced on one path and omitted on the other.Evidence (logs)
Session
sesyj3b7rvwdr, anchordiscord_1494713026509668483:definition_78fc24cbfb20, used by a weekly scheduled task:The task ran successfully and delivered; 91 seconds later its own Session was deleted.
The logs contain 10 such clear events against
definition_*anchors, each matching the last-activity timestamp of one of 5 known-dead Sessions. Example: a clear at07-27 10:01:15corresponds toses7hejj3hraw, whose task failed the next day at 10:00 withagent session id not found: ses7hejj3hraw.Net effect in this environment: 4 enabled definitions orphaned (3 since repaired, 1 still outstanding), plus 17 soft-deleted definitions with the same dangling
session_idthat will never dispatch again.Control case (pins the trigger to visible delivery)
Three definitions minted in the same batch, in the same scope, with the same anchor format, had different fates:
ab2dd2e16504, whose prompt instructs it to reply only with<silent>...</silent>→ never produces a delivery message →scheduled_anchor_message_idisNone→finalize_scheduled_deliveryis never called → its Sessionsesgrd9mx7yu7(anchordiscord_1494713026509668483:definition_8339036c4bba, created2026-07-25T03:19:40) has run successfully for several consecutive days and is still alive.This isolates the trigger to "did the run emit a visible delivery message", and rules out time-based, quota-based, and GC-based explanations.
Reproduction
vibe task add|update --create-session --scope-id discord::channel::<id> ...agent_sessionsand thatrun_definitions.session_idpoints at it.<silent>).agent_sessions— the row is gone.agent session id not found,last_status=failed, 0s duration, emptyresult_text.Expected behavior
clear_provisional_sourceshould apply only to genuinely one-shot provisional anchors, and a hard delete of a Session should not be able to leave a live definition pointing at a dead id.Possible directions (suggestions only — the maintainers should pick the right one):
_build_delivery_alias_strategy, exclude anchors whosesession_anchorcontains:definition_.run_definitions.session_id: do not clear a Session that any enabled definition still references.run_definitions.session_idon the hard-delete path (or at minimum a startup self-check), bringing it in line witharchive_session.Impact
Every user who combines
--create-sessionwith a scheduled task or watch in a Discord channel scope — and, by the same code path, any IM platform whoseshould_use_thread_for_reply()returnsTrue. For any such task that pushes a visible message, Session deletion after the first delivery is deterministic, not intermittent.The only working configuration currently is
session_policy=create_per_runwithrun_definitions.session_id IS NULL, i.e. the definition holds no Session reference at all — which forfeits cross-run conversational memory.Ruled out
max_age/keep_lastpolicy; the only retention constant applies to on-disk backup files.agent_sessions.--create-sessioncall deleting the first. That path is insert-only, and anchors carry a uuid4 suffix so they cannot collide. Thevibe sessioncommand group has no delete subcommand.