Skip to content

Fix Telegram topic-bound subagent session spawns#42707

Closed
daisyportman152 wants to merge 1 commit into
openclaw:mainfrom
daisyportman152:fix-telegram-topic-bound-subagent-spawns
Closed

Fix Telegram topic-bound subagent session spawns#42707
daisyportman152 wants to merge 1 commit into
openclaw:mainfrom
daisyportman152:fix-telegram-topic-bound-subagent-spawns

Conversation

@daisyportman152

Copy link
Copy Markdown

Summary

Fix Telegram topic-bound sessions_spawn(..., thread=true, mode="session") for subagent sessions.

Problem

Telegram thread-bound subagent spawns could fail with Thread bindings are unavailable for telegram. or Session mode is unavailable for this target. because the Telegram extension was missing subagent lifecycle wiring, requester topic context could be lost on agent --session-id runs, and session-binding state could be split across module instances.

Fix

Register Telegram subagent lifecycle hooks, hydrate requester context from persisted session delivery state, share session-binding adapters and services across module instances, and prewarm the Telegram thread-binding manager before capability checks.

Validation

  • pnpm exec vitest run extensions/telegram/index.test.ts extensions/telegram/src/subagent-hooks.test.ts
  • pnpm exec vitest run src/infra/outbound/session-binding-service.test.ts src/telegram/thread-bindings.test.ts
  • pnpm exec vitest run src/commands/agent.test.ts -t "hydrates telegram topic routing from persisted session delivery context on session resume"
  • pnpm build
  • pnpm check currently still fails on an unrelated repo error at src/cron/isolated-agent/run.ts(207,12): Cannot find name 'CommandLane'.

@openclaw-barnacle openclaw-barnacle Bot added channel: telegram Channel integration: telegram cli CLI command changes commands Command implementations size: L labels Mar 11, 2026
@greptile-apps

greptile-apps Bot commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR wires up the missing Telegram subagent lifecycle hooks (subagent_spawning, subagent_delivery_target, subagent_ended) so that thread-bound sessions_spawn(..., thread=true, mode="session") calls work correctly for Telegram topics. It also fixes two root causes of the failure: session context hydration is now restored from persisted deliveryContext on --session-id resumes (run-context.ts, agent.ts), and the session-binding adapter registry + service singleton are migrated to globalThis-backed storage so they are shared across module instances rather than siloed per-module-load.

Key changes:

  • extensions/telegram/src/subagent-hooks.ts (new): Implements the three subagent lifecycle hooks; the subagent_spawning handler prewarms the thread-binding manager, checks binding capabilities, and calls bind; the subagent_delivery_target handler resolves the delivery origin from active bindings; the subagent_ended handler unbinds on session close.
  • src/infra/outbound/session-binding-service.ts: Replaces module-level Map and singleton with globalThis-backed equivalents so all module instances share the same adapter registry and service.
  • src/commands/agent/run-context.ts / src/commands/agent.ts: Hydrates messageChannel, accountId, to, and threadId from the session's persisted deliveryContext when resuming via --session-id, enabling correct Telegram topic routing on resume.
  • src/telegram/thread-bindings.ts: Adds ensureTelegramThreadBindingManager as a public prewarm helper; delegates to the existing createTelegramThreadBindingManager (which is already idempotent).

One test has an order-dependent initialization that could cause it to fail if run in isolation.

Confidence Score: 5/5

  • Safe to merge — all changes are additive, well-tested, and architecturally sound.
  • The PR correctly implements subagent lifecycle hooks for Telegram, properly handles session context persistence, and uses sound patterns for shared global state. The single finding is a test isolation issue that is quickly fixed and doesn't impact production correctness.
  • src/infra/outbound/session-binding-service.test.ts (test initialization order dependency)

Last reviewed commit: a0784d5

Comment on lines +202 to +224
it("shares adapter and service state through globalThis", () => {
registerSessionBindingAdapter({
channel: "discord",
accountId: "default",
capabilities: {
placements: ["current"],
},
bind: async (input) => createRecord(input),
listBySession: () => [],
resolveByConversation: () => null,
unbind: async () => [],
});

const globalAdapters = Reflect.get(
globalThis,
"__openclawSessionBindingAdaptersByChannelAccount",
);
const globalService = Reflect.get(globalThis, "__openclawSessionBindingService");

expect(globalAdapters).toBeInstanceOf(Map);
expect((globalAdapters as Map<string, unknown>).has("discord:default")).toBe(true);
expect(globalService).toBe(getSessionBindingService());
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test has a hidden order dependency. globalThis.__openclawSessionBindingService is only written the first time getSessionBindingService() is called (lazy initialization). The adapters map is written at module-load time, but the service is not.

If this test runs in isolation (e.g. via it.only, test randomization, or in a test run before other tests that invoke getSessionBindingService()), the globalService variable would be undefined, and the final assertion would fail:

expect(undefined).toBe(getSessionBindingService()) // fails

Initialize the service first to ensure globalThis is populated:

Suggested change
it("shares adapter and service state through globalThis", () => {
registerSessionBindingAdapter({
channel: "discord",
accountId: "default",
capabilities: {
placements: ["current"],
},
bind: async (input) => createRecord(input),
listBySession: () => [],
resolveByConversation: () => null,
unbind: async () => [],
});
const globalAdapters = Reflect.get(
globalThis,
"__openclawSessionBindingAdaptersByChannelAccount",
);
const globalService = Reflect.get(globalThis, "__openclawSessionBindingService");
expect(globalAdapters).toBeInstanceOf(Map);
expect((globalAdapters as Map<string, unknown>).has("discord:default")).toBe(true);
expect(globalService).toBe(getSessionBindingService());
});
const service = getSessionBindingService(); // Ensure globalThis is populated
const globalAdapters = Reflect.get(
globalThis,
"__openclawSessionBindingAdaptersByChannelAccount",
);
const globalService = Reflect.get(globalThis, "__openclawSessionBindingService");
expect(globalAdapters).toBeInstanceOf(Map);
expect((globalAdapters as Map<string, unknown>).has("discord:default")).toBe(true);
expect(globalService).toBe(service);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/outbound/session-binding-service.test.ts
Line: 202-224

Comment:
This test has a hidden order dependency. `globalThis.__openclawSessionBindingService` is only written the first time `getSessionBindingService()` is called (lazy initialization). The adapters map is written at module-load time, but the service is not.

If this test runs in isolation (e.g. via `it.only`, test randomization, or in a test run before other tests that invoke `getSessionBindingService()`), the `globalService` variable would be `undefined`, and the final assertion would fail:
```
expect(undefined).toBe(getSessionBindingService()) // fails
```

Initialize the service first to ensure globalThis is populated:

```suggestion
const service = getSessionBindingService(); // Ensure globalThis is populated

    const globalAdapters = Reflect.get(
      globalThis,
      "__openclawSessionBindingAdaptersByChannelAccount",
    );
    const globalService = Reflect.get(globalThis, "__openclawSessionBindingService");

    expect(globalAdapters).toBeInstanceOf(Map);
    expect((globalAdapters as Map<string, unknown>).has("discord:default")).toBe(true);
    expect(globalService).toBe(service);
```

How can I resolve this? If you propose a fix, please make it concise.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a0784d52f6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +55 to +59
const target = rawTo.startsWith("channel:")
? rawTo.slice(8)
: rawTo.startsWith("telegram:")
? rawTo.slice(9)
: rawTo;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize legacy Telegram target prefixes before binding

resolveTelegramConversationId only strips channel: and telegram:, so valid Telegram target forms like telegram:group:-100123:topic:55 (and tg: aliases) are converted into conversation IDs such as group:-100123:topic:55 instead of -100123:topic:55. That malformed ID then propagates into completion routing (e.g. channel:group:-100123), which the Telegram send path cannot resolve to a real chat, so thread-bound subagent session replies can be dropped for those supported target formats.

Useful? React with 👍 / 👎.

@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Apr 27, 2026
@clawsweeper

clawsweeper Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

This stale PR should close as superseded: current main still lacks the Telegram subagent hook implementation, but the same remaining work is now tracked by the canonical issue and a newer active PR that rewrites the approach around the current bundled-channel boundary. The useful global session-binding part is already present on main, while the old branch still has correctness findings and missing live Telegram proof.

Canonical path: Keep #42986 as the tracker and review, land, or replace #82023 as the canonical current-boundary implementation with redacted live Telegram proof.

So I’m closing this here and keeping the remaining discussion on #42986 and #82023.

Review details

Best possible solution:

Keep #42986 as the tracker and review, land, or replace #82023 as the canonical current-boundary implementation with redacted live Telegram proof.

Do we have a high-confidence way to reproduce the issue?

Yes, at source level: current main still gates thread-bound native subagent sessions on subagent_spawning hooks, and Telegram registers no such hooks while its binding adapter exists. I did not run a live Telegram reproduction in this read-only review.

Is this the best way to solve the issue?

No. The old branch is not the best current solution because it uses the obsolete plugin.register/core-internal boundary and duplicates work now covered by the newer registerFull/per-kind-capability PR.

Security review:

Security review cleared: No concrete security or supply-chain issue was found; the diff adds no dependencies, workflows, lifecycle scripts, downloaded artifacts, or new secret storage.

What I checked:

Likely related people:

  • vincentkoc: Current blame for the Telegram entry, session-binding adapter, spawn gate, and spawn-policy helpers points to Vincent Koc's recent plugin/runtime boundary commit; the same history includes Telegram binding cleanup work. (role: recent area contributor; confidence: high; commits: 89a9b4e75acd, c441dcd47a05; files: extensions/telegram/index.ts, extensions/telegram/src/thread-bindings.ts, src/agents/subagent-spawn.ts)
  • steipete: Recent history on the Telegram thread-binding file includes several routing, persistence, runtime seam, and lazy-load changes by Peter Steinberger adjacent to this hook work. (role: recent Telegram thread-binding contributor; confidence: medium; commits: 96fe85fb77, 33e93e2a07, 79f02b6e54; files: extensions/telegram/src/thread-bindings.ts)
  • huntharo: History ties Harold Hunt to the Telegram ACP topic binding and spawn parameter parsing work that introduced or shaped the topic-binding primitives used by this feature area. (role: Telegram topic-binding feature-history contributor; confidence: medium; commits: d58dafae88, 5eb99a9b50; files: extensions/telegram/src/thread-bindings.ts, extensions/telegram/src/channel.ts)

Codex review notes: model gpt-5.5, reasoning high; reviewed against 33685e14748b.

@openclaw-barnacle openclaw-barnacle Bot removed the stale Marked as stale due to inactivity label Apr 28, 2026
@clawsweeper clawsweeper Bot added the mantis: telegram-visible-proof Mantis should capture Telegram visible proof. label May 10, 2026
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 10, 2026
@clawsweeper clawsweeper Bot added mantis: telegram-visible-proof Mantis should capture Telegram visible proof. and removed mantis: telegram-visible-proof Mantis should capture Telegram visible proof. labels May 10, 2026
@clawsweeper clawsweeper Bot closed this May 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: telegram Channel integration: telegram cli CLI command changes commands Command implementations mantis: telegram-visible-proof Mantis should capture Telegram visible proof. size: L triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant