Skip to content

Integrate Bitbucket master-v1: Telegram continuity, token counter, reasoning render (+3 fixes) - #98

Merged
kartikmanimuthu merged 10 commits into
master-v1from
merge-integration
Jul 25, 2026
Merged

kartikmanimuthu merged 10 commits into
master-v1from
merge-integration

Conversation

@kartikmanimuthu

Copy link
Copy Markdown
Collaborator

Integrates Omar Hussain's work from the Bitbucket remote (rohitahuja1/smc-stx-nucleus-cloud-ops), merged there as PR #1.

Both repos share commit history and the Nx layout, so this is a true git merge rather than a path remap — they diverged at 4aa59f36.

Commits integrated

Commit Change
47ba4add Rewrite empty-after-strip reasoning messages to a placeholder (Bedrock role-alternation)
3df80a90 Render array-content reasoning as thinking blocks in history
14fdc747 Chat input cap 2000→6000, default output tokens 4096→8192
6716c282 Telegram tenant resolution via new telegram_bot_links table + gateway webhook auth bypass
25869751 Telegram conversational continuity + /new reset
825d81ff Live + persisted token counter in chat header

apps/web-ui/components/agent/workspace/composer.tsx was the only file touched by both sides (our MicButton import/JSX vs the MAX_CHARS bump). It auto-merged cleanly and the dictation button now inherits the 6000-char cap.

Fixes applied on top of the merged result

1. Narrow the gateway auth exemption (critical)

6716c282 exempted all of api/v1/gateway from the NextAuth matcher so Telegram's webhook could reach the adapter. That also exempted api/v1/gateway/api and api/v1/gateway/stream.

Two things compound there:

  • ApiAdapter.validateRequest returns true for any Authorization or x-api-key header without validating its value.
  • middleware.ts is what overwrites a client-supplied x-tenant-id with the session's real tenantId — skipping the matcher means the client's header reaches the route verbatim.

Net effect: an unauthenticated caller could start an agent run under an arbitrary tenant, with that tenant's AWS credentials and LLM provider.

Narrowed the exemption to the five externally-signed webhook channels (Telegram secret token, Slack HMAC, Discord Ed25519, Jira/webhook shared secret), each of which verifies its own signature in validateRequest. Verified all 12 path cases against the compiled matcher regex from .next/server/middleware-manifest.json.

2. Restore three dropped production indexes

6716c282's migration was generated by prisma migrate dev, which cannot see three hand-written raw-SQL indexes (pgvector Unsupported("vector") / tsvector columns have no Prisma index syntax). It read them as drift and emitted DROP INDEX alongside the intended table:

  • agent_memories_embedding_hnsw — memory recall
  • idx_inventory_search_vector — inventory full-text search
  • idx_kb_document_chunks_embedding — KB semantic search

Dropping them is silent: queries stay correct but fall back to sequential scans. Confirmed on a scratch pgvector database — 0 of 3 survive the chain without a fix, 3 of 3 with it.

Added 20260725120000_restore_raw_sql_indexes, which recreates all three with IF NOT EXISTS. The upstream migration is left byte-identical so an already-applied deploy does not hit a prisma migrate deploy checksum mismatch.

3. Fix the char-counter test

The 2000→6000 cap bump left composer.test.tsx asserting '1900/2000'. Updated to the new thresholds.

Verification

  • tsc (web-ui): 0 TS2307. The 6 errors in touched files are byte-identical pre-merge (line numbers shift exactly by the insertions above).
  • vitest (web-ui): 19 failed / 1330 passed, vs a pre-merge baseline worktree at 19 failed / 1295 passed — identical failure set (all DATABASE_URL-dependent), +35 new passing tests.
  • workers: typecheck clean; 5 failures vs 7 pre-merge.
  • bun run build:web: succeeds.
  • Migrations: full chain applies clean on a fresh pgvector/pgvector:pg16 container; end state asserted via pg_indexes. The local dev database was never touched.

Follow-ups (not in this PR)

  • Pre-existing, unfixed: api/v1/trigger is exempted wholesale and api/v1/trigger/api/route.ts is a live back-compat alias calling the same handleInbound('api', …), so the same unauthenticated-run path exists there today. The real fix is validating the API key in ApiAdapter.validateRequest (or deleting the alias), not another matcher tweak.
  • Not integrated: Omar's deployment/to-sandbox-env branch (6 infra-only commits parameterizing both Pulumi stacks by appName/natStrategy for an isolated sbx environment) is unmerged on Bitbucket and deliberately left out.

🤖 Generated with Claude Code

Omar Hussain and others added 10 commits July 21, 2026 18:00
A turn-1 reasoning-only AI message (content is a lone reasoning block, no
text, no tool_calls) becomes empty after stripReasoningBlocks() and crashed
Bedrock on turn 2 — and on history-session resume — with "The content field
in the Message object ... is empty". Both paths replay the LangGraph
checkpoint through sanitizeMessagesForBedrock(), so both hit it.

Fix: in sanitizeMessagesForBedrock(), when an AI message is empty after
stripping and has no tool calls, rewrite its content to "(reasoning omitted)"
instead of leaving it empty. Rewriting (not dropping) preserves message count
and Human/AI role alternation, which getRecentMessages enforces upstream and
Bedrock requires. Adds a module-private isEmptyAiMessageContent() predicate
that leaves tool-use turns and normal answers untouched.

Includes design spec + implementation plan under docs/superpowers/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reopened history sessions rendered extended-thinking as a raw
[{"type":"reasoning",...}] JSON blob. The write path JSON.stringify's AI
messages whose content is a content-block array, and the read path only kept
text blocks, so a pure-reasoning array fell through as a plain text answer.

Add a read-path helper (lib/agent-chat/ai-content-parts.ts) that reconstructs
such arrays block-by-block into reasoning/text parts — dropping empty-text
reasoning to match the live reducer — and wire it into the AI branch of
convertPlainMessage and convertMessage in the history route, with the existing
string/marker path as the untouched fallback. Read-path only: storage, the
write path, and the live stream are unchanged, so it fixes existing and future
sessions with no migration. Gated to the AI role; human/tool paths untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…kens to 8192

- Chat composer input limit 2000 -> 6000 chars (counter warning 1800 -> 5400).
- DEFAULT_MAX_OUTPUT_TOKENS 4096 -> 8192 (fallback when a ProviderModel record
  sets no maxTokens); Bedrock/Anthropic Claude models used by AI Ops support it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Inbound Telegram webhooks were being redirected to /login by the NextAuth
middleware (gateway routes were never exempted from the auth matcher, unlike
api/v1/trigger), and even once reachable, the adapter resolved tenantId from
Telegram's chat.id instead of an actual tenant — a value that never matches,
so every request was rejected as unauthorized.

Adds TelegramBotLink (secretToken -> tenantId), mirroring the existing
SlackWorkspaceLink pattern, since Telegram's payload carries no tenant-
identifying field of its own. Also stops the gateway from returning a hard
400 for text-less messages (e.g. Telegram's automatic /start) on async
"streaming" channels, since a non-2xx there makes Telegram retry the same
update forever and blocks its whole queue behind it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A Telegram chat now behaves as an ongoing conversation. When a run is
awaiting the user's input (agent asked a clarifying question), the next
message is fed into that same run/thread instead of spawning a new run —
so answers keep context. Once a run has completed and returned a result,
the next message starts a fresh run.

- findResumableTelegramRun(chatId, idleCutoff): resolves the chat's run
  currently awaiting input, within a 30-min idle window (stale questions
  are abandoned). Keyed on trigger.chatId, source=telegram.
- /new (and /n): cancels a pending awaiting-input run and confirms, so the
  next message starts fresh rather than being read as the answer.
- gateway: 'reset' action wired; adapter gains sendSessionReset.

Fixes the prior behavior where every Telegram message — including a reply
to the agent's own clarifying question — created a separate run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Show a cumulative incoming/outgoing token total in the AI Ops chat header,
next to the elapsed timer. The counter sums usage_metadata (already produced by
every Bedrock/Claude call but previously discarded) across all model calls.

- Server: accumulate a request-scoped run total across every non-guard
  on_chat_model_end, stream a data-usage part per call (live), and persist the
  run total onto the turn's final AI message (metadata.usage_metadata).
- History: reconstruct one data-usage part per turn from the stored total so the
  count survives reload and shows when reopening a session.
- Client: deriveRunState sums data-usage parts into RunState.tokenUsage; the
  header renders "↓ in · ↑ out" (hidden at zero, exact counts on hover).

Read-path/stream/persist only; no schema change. Pure helpers
(parseUsageMetadata/formatTokens) and the reducer accumulation are unit-tested;
live==reload parity verified by review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ing render

Integrates Omar Hussain's work from the Bitbucket remote
(rohitahuja1/smc-stx-nucleus-cloud-ops), merged there as PR #1:

  47ba4ad fix(aiops): rewrite empty-after-strip reasoning messages to placeholder
  3df80a9 fix(aiops): render array-content reasoning as thinking blocks in history
  14fdc74 feat(aiops): raise chat input cap to 6000 chars, default output tokens 8192
  6716c28 fix(agent-ops): Telegram tenant resolution + gateway webhook auth bypass
  2586975 feat(agent-ops): Telegram conversational continuity + /new reset
  825d81f feat(aiops): live + persisted token counter in chat header

Both repos share history; they diverged at 4aa59f3, so this is a true
merge. The only file touched by both sides — workspace/composer.tsx —
auto-merged cleanly (our MicButton import/JSX vs Omar's MAX_CHARS bump);
the dictation button now inherits the new 6000-char cap.

Three fixes applied on top of the merged result:

1. middleware.ts — Omar exempted all of `api/v1/gateway` from the NextAuth
   gate so Telegram's webhook could reach the adapter. That also exempted
   `api/v1/gateway/api` and `api/v1/gateway/stream`. ApiAdapter.validateRequest
   accepts ANY Authorization/x-api-key header without validating it, and this
   middleware is what overwrites a client-supplied x-tenant-id with the
   session's real tenantId — so the wider matcher let an unauthenticated
   caller start an agent run under an arbitrary tenant. Narrowed the exemption
   to the five externally-signed webhook channels, which verify their own
   signature/secret. Verified against the compiled matcher regex.

2. libs/prisma/migrations/20260725120000_restore_raw_sql_indexes — Omar's
   telegram migration was generated by `prisma migrate dev`, which read three
   hand-written raw-SQL indexes as drift and emitted DROP INDEX for them
   (agent_memories HNSW, inventory searchVector GIN, kb_document_chunks
   IVFFlat). Dropping them is silent: queries stay correct but fall back to
   sequential scans. Confirmed on a scratch database — 0 of 3 survive the
   chain without this migration, 3 of 3 with it. The upstream migration is
   left byte-identical so an already-applied deploy does not hit a checksum
   mismatch; this forward migration is idempotent.

3. composer.test.tsx — the 2000 -> 6000 cap bump left the char-counter test
   asserting '1900/2000'. Updated to the new thresholds.

Verification: web-ui tsc clean of new errors (0 TS2307; the 6 errors in
touched files are byte-identical pre-merge). web-ui vitest 19 failed /
1330 passed vs a pre-merge baseline of 19 failed / 1295 passed — identical
failure set (all DATABASE_URL-dependent), +35 new passing tests. workers
typecheck clean, 5 failures vs 7 pre-merge. `bun run build:web` succeeds.
Full migration chain applies clean on a fresh pgvector database.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kartikmanimuthu
kartikmanimuthu merged commit 8dddd59 into master-v1 Jul 25, 2026
1 check failed
@kartikmanimuthu
kartikmanimuthu deleted the merge-integration branch July 25, 2026 18:12
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.

1 participant