Skip to content

Chat Phase 2b-2b/c — ephemeral messages + in-app help + all-threads + lightbox zoom#241

Merged
jaylfc merged 12 commits intomasterfrom
feat/chat-phase-2b-2b-ephemeral-polish
Apr 20, 2026
Merged

Chat Phase 2b-2b/c — ephemeral messages + in-app help + all-threads + lightbox zoom#241
jaylfc merged 12 commits intomasterfrom
feat/chat-phase-2b-2b-ephemeral-polish

Conversation

@jaylfc
Copy link
Copy Markdown
Owner

@jaylfc jaylfc commented Apr 20, 2026

Summary

Bundles 4 independent features from the original Phase 2b-2 polish bucket, minus read receipts (dropped — Slack/Discord don't do them; unread badges cover the use case).

Ephemeral messages (Signal/WhatsApp style)

  • `chat_messages.expires_at REAL` column + `send_message(..., expires_at=)`.
  • Routes auto-compute expires_at from `channel.settings.ephemeral_ttl_seconds`.
  • 5-minute background sweep soft-deletes expired messages + broadcasts `message_delete` WS events.
  • Channel settings: "Disappearing messages" dropdown (Off / 1h / 24h / 7d / 30d).
  • Channel header badge: `⏳ 24h` when TTL is set.

In-app help panel

  • `GET /api/docs/chat-guide` serves markdown from `docs/chat-guide.md`.
  • `HelpPanel` modal replaces the external "?" link — Esc + backdrop close.

All-threads list

  • `GET /api/chat/channels/{id}/threads` — returns thread parents with reply counts.
  • `AllThreadsList` right-side panel, mutex with settings + thread panels, jump-to-thread.
  • `💬` button in channel header (non-DM only).

Lightbox zoom + pan

  • Keyboard `+` / `-` / `0`, mouse wheel, double-click toggle 1x/2x.
  • Pan when zoomed > 1x. Max 4x, min 1x.
  • Resets on image nav (← / →).

Test plan

  • Backend: 38 tests across ephemeral / threads / docs — all pass.
  • Frontend: 5 new component tests + full suite 302/305 (3 pre-existing snap-zones unrelated).
  • Bundle rebuilt.
  • Playwright E2E stubs; gated on `TAOS_E2E_URL`.
  • Manual: set channel to 1h TTL, send message, verify expires_at and disappearance after an hour; open help panel; open all-threads; zoom lightbox.

Summary by CodeRabbit

Release Notes

  • New Features
    • Added disappearing message support with configurable time-to-live (TTL) settings per channel, displayed via a badge in the channel header.
    • Introduced an in-app Chat Guide accessible via a help button, replacing the external link.
    • Added an All Threads panel to browse all discussions within a channel.
    • Enhanced image viewer with zoom and pan controls via keyboard, mouse wheel, and pointer interactions.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 20, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6bab2dbb-b4e2-4f4b-9724-97f91722b51f

📥 Commits

Reviewing files that changed from the base of the PR and between d079378 and 923dac6.

📒 Files selected for processing (14)
  • desktop/src/apps/chat/AllThreadsList.tsx
  • desktop/src/apps/chat/HelpPanel.tsx
  • static/desktop/assets/MCPApp-B1Gkthyy.js
  • static/desktop/assets/MessagesApp-C046a0HU.js
  • static/desktop/assets/ProvidersApp-aCbJgn9_.js
  • static/desktop/assets/SettingsApp-B0JGP8eU.js
  • static/desktop/assets/chat-r4kXbV-3.js
  • static/desktop/assets/main-B4042Q04.js
  • static/desktop/assets/tokens-ohTp7OPq.css
  • static/desktop/assets/tokens-xmxHxvia.js
  • static/desktop/chat.html
  • static/desktop/index.html
  • tinyagentos/chat/channel_store.py
  • tinyagentos/routes/chat.py

📝 Walkthrough

Walkthrough

This PR introduces four major chat enhancements: ephemeral message TTL with channel-level settings and automatic background sweeping, an in-app help panel with markdown rendering replacing external links, an all-threads list panel for browsing channel conversations, and image zoom/pan controls in the attachment lightbox. Backend support includes message expiration persistence, thread metadata endpoints, and periodic cleanup loops.

Changes

Cohort / File(s) Summary
Ephemeral Messages — Frontend UI
desktop/src/apps/MessagesApp.tsx, desktop/src/apps/chat/ChannelSettingsPanel.tsx
Extended channel settings to include ephemeral_ttl_seconds field; added TTL display badge in channel header and dropdown selector in settings panel; introduced formatTTL() helper to convert seconds to m/h/d/w strings.
Ephemeral Messages — Backend & Storage
tinyagentos/chat/message_store.py, tinyagentos/chat/channel_store.py, tinyagentos/routes/chat.py, tinyagentos/app.py
Extended chat_messages schema with expires_at column; added sweep_expired() to soft-delete expired messages and get_channel_threads() to fetch thread metadata; wired TTL into message creation paths; added background sweep loop that periodically calls sweep_expired() and broadcasts message_delete events to hub.
All Threads Panel — Frontend
desktop/src/apps/chat/AllThreadsList.tsx, desktop/src/apps/MessagesApp.tsx
New AllThreadsList component fetches thread summaries from /api/chat/channels/{id}/threads with loading/error/empty states and relative timestamps; integrated into MessagesApp with showAllThreads state, header button, and mutex logic to coordinate panel visibility with settings and thread views.
Help Panel — Frontend
desktop/src/apps/chat/HelpPanel.tsx, desktop/src/apps/MessagesApp.tsx
New HelpPanel component replaces external "?" link; fetches guide markdown from /api/docs/chat-guide with abort controller, renders markdown with inline code/bold/links, and supports Escape key and backdrop-click dismissal; integrated into MessagesApp with state/event handlers.
Image Lightbox Enhancements
desktop/src/apps/chat/AttachmentLightbox.tsx
Added zoom/pan state tracking with min/max clamping; implemented wheel zoom with preventDefault, double-click 1x/2x toggle, pointer-drag panning when zoomed; added keyboard shortcuts (+/-/0) for zoom control; reset zoom/pan on image navigation; replaced single-line UI with explicit Previous/Next buttons.
Backend Endpoints
tinyagentos/routes/chat.py
Added GET /api/docs/chat-guide to serve chat guide markdown; added GET /api/chat/channels/{channel_id}/threads to return thread metadata; extended message creation and channel settings PATCH to compute and persist expires_at based on channel TTL.
Frontend Type Definitions
desktop/src/lib/channel-admin-api.ts
Extended ChannelPatch type with optional ephemeral_ttl_seconds: number | null field to support TTL updates in PATCH requests.
Frontend Tests
desktop/src/apps/chat/__tests__/AllThreadsList.test.tsx, desktop/src/apps/chat/__tests__/HelpPanel.test.tsx
Added Vitest suites for AllThreadsList covering empty/populated thread states and user interactions; added suite for HelpPanel validating fetch, render, Escape/backdrop dismissal, and accessible dialog markup.
Backend Tests
tests/test_chat_ephemeral.py, tests/test_chat_threads.py, tests/test_chat_docs.py
Added async pytest tests for sweep_expired() behavior (persistence/defaults/expiration), get_channel_threads() endpoint, and /api/docs/chat-guide markdown fetch and content validation.
E2E Tests
tests/e2e/test_chat_phase2b2b.py
Added Playwright tests validating help panel open/close via UI and Escape key, and all-threads panel visibility/closure.
Documentation
docs/superpowers/plans/2026-04-20-chat-phase-2b-2b-ephemeral-polish.md, docs/superpowers/specs/2026-04-20-chat-phase-2b-2b-ephemeral-polish-design.md
Added comprehensive plan and design specification for ephemeral TTL, in-app help, all-threads, and lightbox enhancements with implementation details.
Build Artifacts
static/desktop/assets/MessagesApp-C046a0HU.js (added), static/desktop/assets/MessagesApp-CkhTvSE9.js (removed), static/desktop/assets/tokens-ohTp7OPq.css (added), static/desktop/assets/tokens-DbWedNtZ.css (removed), static/desktop/assets/{MCPApp,ProvidersApp,SettingsApp,chat-r4kXbV-3}.js, static/desktop/{chat,index}.html
Rebuilt bundled assets: new MessagesApp bundle includes ephemeral UI, help/threads panels, and lightbox zoom; updated main/tokens hashes across all entry points and dependent bundles; refreshed Tailwind CSS bundle with latest utilities.

Sequence Diagrams

sequenceDiagram
    participant Client as Frontend:<br/>MessagesApp
    participant API as Backend:<br/>Routes
    participant Store as Backend:<br/>Message Store
    participant Hub as WebSocket Hub
    
    Client->>API: POST /api/chat/messages<br/>(with channel TTL)
    API->>Store: send_message(...,<br/>expires_at=now+ttl)
    Store-->>API: message with expires_at
    API-->>Client: message ACK
    
    Note over Store: Background Loop<br/>(every 5 min)
    Store->>Store: sweep_expired()
    Store-->>API: [(msg_id, ch_id), ...]
    
    loop For each expired message
        API->>Hub: broadcast message_delete<br/>event to channel
        Hub-->>Client: message_delete event
        Client->>Client: remove message from UI
    end
Loading
sequenceDiagram
    participant User as User
    participant Client as Frontend:<br/>MessagesApp
    participant API as Backend:<br/>Routes
    participant FS as Filesystem
    
    User->>Client: Click "Chat Guide" button
    Client->>Client: setShowHelp(true)
    Client->>Client: Render HelpPanel
    
    HelpPanel->>API: GET /api/docs/chat-guide
    API->>FS: Read docs/chat-guide.md
    FS-->>API: markdown content
    API-->>HelpPanel: {markdown: "..."}
    
    HelpPanel->>HelpPanel: Parse & render markdown<br/>to React nodes
    HelpPanel-->>Client: Rendered HTML
    
    User->>Client: Press Escape or click backdrop
    Client->>Client: setShowHelp(false)
    Client->>Client: Close HelpPanel
Loading
sequenceDiagram
    participant User as User
    participant Client as Frontend:<br/>MessagesApp
    participant API as Backend:<br/>Routes
    participant Store as Backend:<br/>Message Store
    
    User->>Client: Click "All threads" button
    Client->>Client: setShowAllThreads(true)
    Client->>Client: Render AllThreadsList
    
    AllThreadsList->>API: GET /api/chat/channels/{id}/threads
    API->>Store: get_channel_threads(channel_id)
    Store-->>API: [{id, content, author,<br/>reply_count, last_reply_at}, ...]
    API-->>AllThreadsList: {threads: [...]}
    
    AllThreadsList->>AllThreadsList: Render thread rows<br/>with timestamps
    AllThreadsList-->>Client: Thread list UI
    
    User->>AllThreadsList: Click a thread
    AllThreadsList->>Client: onJumpToThread(parent_id)
    Client->>Client: Close AllThreadsList,<br/>Open Thread Panel
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Hop through the threads, they've vanish away,
Help guides appear in the chat—hooray!
Zoom in the photos, pan left and right,
Messages expire when TTL's set tight!
Four features bundled, one PR to delight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately describes the main changes: it lists all four major features implemented (ephemeral messages, in-app help, all-threads, lightbox zoom), using concise, clear terminology without vagueness or noise.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/chat-phase-2b-2b-ephemeral-polish

Comment @coderabbitai help to get the list of available commands and usage tips.

@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot bot commented Apr 20, 2026

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Incremental Update

All issues from previous review have been fixed correctly:

  • ✅ Added AbortController with proper cleanup on unmount/re-render for fetch calls in both components
  • ✅ Properly handle AbortError and avoid setting state on unmounted components
  • ✅ Implemented safe markdown rendering with proper escaping
  • ✅ Fixed all memory leak patterns in useEffect hooks
Files Reviewed (2 files changed)
  • desktop/src/apps/chat/AllThreadsList.tsx - ✅ Fixed
  • desktop/src/apps/chat/HelpPanel.tsx - ✅ Fixed

Reviewed by seed-2-0-pro-260328 · 146,198 tokens

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 16

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
desktop/src/apps/chat/AttachmentLightbox.tsx (1)

70-75: ⚠️ Potential issue | 🟡 Minor

Add modal semantics to the lightbox.

This overlay behaves like a blocking dialog, but it is missing aria-modal="true", so assistive tech can still traverse background content while the viewer is open.

♿ Proposed fix
     <div
       role="dialog"
+      aria-modal="true"
       aria-label="Image viewer"
       className="fixed inset-0 z-50 bg-black/80 flex items-center justify-center"
       onClick={onClose}
       onWheel={handleWheel}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/chat/AttachmentLightbox.tsx` around lines 70 - 75, The
dialog overlay in AttachmentLightbox.tsx (the div with role="dialog" and
handlers onClose and handleWheel) is missing aria-modal="true"; update that
element to include aria-modal="true" so assistive tech treats it as a blocking
modal while the lightbox is open. Ensure the attribute is added alongside
role="dialog" and aria-label="Image viewer" on the same div.
tinyagentos/routes/chat.py (1)

76-97: ⚠️ Potential issue | 🟠 Major

TTL computation is still easy to miss on other message paths.

Expiry is derived only in this websocket branch and the main HTTP branch. The /help system-message path in this file returns before that logic runs, and tinyagentos/agent_chat_router.py:178-185 still calls send_message(...) without expires_at, so some messages in ephemeral channels will never disappear. This needs to be centralized behind a shared helper/service boundary instead of being repeated per route.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tinyagentos/routes/chat.py` around lines 76 - 97, The TTL/expiry computation
for ephemeral channels is duplicated and missing on some code paths (websocket
message branch, the /help system-message path and the call from
agent_chat_router to msg_store.send_message), so extract the logic that reads
channel settings and computes expires_at into a shared helper (e.g.,
compute_expires_at(channel_store, channel_id) or
ChannelExpiryService.get_expires_at) that calls get_channel and returns the
proper expires_at or None; then call that helper from the websocket message
handler (where _ws_ttl/_ws_expires_at are currently computed), from the /help
system-message path, and from the code that invokes send_message in
agent_chat_router (so every call to send_message uses the helper to pass
expires_at consistently).
🟡 Minor comments (7)
docs/superpowers/specs/2026-04-18-framework-update-phase-1-design.md-54-54 (1)

54-54: ⚠️ Potential issue | 🟡 Minor

Use canonical product capitalization for consistency.

Prefer GitHub:<owner>/<repo> in the scheme description to match naming used elsewhere in the spec.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/specs/2026-04-18-framework-update-phase-1-design.md` at line
54, Update the scheme description for the release_source field to use canonical
product capitalization: change the example scheme from "github:<owner>/<repo>"
to "GitHub:<owner>/<repo>" so the spec consistently uses "GitHub" everywhere
(locate the table row for `release_source` and edit the string in that cell).
docs/superpowers/plans/2026-04-18-framework-update-phase-1.md-1650-1653 (1)

1650-1653: ⚠️ Potential issue | 🟡 Minor

Build instructions use a machine-specific absolute path.

cd /Volumes/NVMe/... is not portable for other developers or CI runners. Prefer repository-relative commands only.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-04-18-framework-update-phase-1.md` around lines
1650 - 1653, The build step uses a machine-specific absolute path ("cd
/Volumes/NVMe/Users/jay/Development/tinyagentos") which breaks portability;
change it to a repository-relative command or remove the explicit cd and run
from the repo root (e.g., use repo root resolution like git rev-parse
--show-toplevel or relative paths) so CI/devs can run the script reliably, and
keep the subsequent git add lines referencing "static/desktop" and
"desktop/tsconfig.tsbuildinfo" as relative paths.
docs/superpowers/plans/2026-04-18-framework-update-phase-1.md-1380-1413 (1)

1380-1413: ⚠️ Potential issue | 🟡 Minor

onUpdated is defined but never used in the FrameworkTab plan snippet.

After a successful update start, the parent view won’t get notified, so sidebar/store indicators can stay stale until a full refresh.

Suggested change
   async function doUpdate() {
     setSubmitting(true);
-    try { await startFrameworkUpdate(agent.name); await load(); }
+    try {
+      await startFrameworkUpdate(agent.name);
+      await load();
+      onUpdated();
+    }
     catch (e: any) { setErr(String(e)); }
     finally { setSubmitting(false); setConfirming(false); }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-04-18-framework-update-phase-1.md` around lines
1380 - 1413, The component FrameworkTab receives onUpdated but never calls it;
modify the doUpdate flow so the parent is notified when an update is
successfully initiated: inside the doUpdate function (after awaiting
startFrameworkUpdate(agent.name) and successfully reloading via load()), call
onUpdated() (ensure it's invoked only on success, e.g., inside the try block
after await load()); this uses existing symbols FrameworkTab, doUpdate, load,
and startFrameworkUpdate to locate where to add the call.
tests/test_chat_threads.py-241-241 (1)

241-241: ⚠️ Potential issue | 🟡 Minor

Drop the unused app binding in this test.

Line 241 unpacks app but never uses it, which triggers lint noise and obscures intent.

🔧 Proposed fix
-    app, client = await _authed_thread_client(tmp_path)
+    _, client = await _authed_thread_client(tmp_path)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_chat_threads.py` at line 241, The test unpacks an unused app from
the coroutine result of _authed_thread_client, causing lint noise; change the
unpack to discard the unused binding (e.g., replace "app, client = await
_authed_thread_client(tmp_path)" with "_, client = await
_authed_thread_client(tmp_path)") so only client is used while keeping the call
to _authed_thread_client for setup.
tinyagentos/chat/message_store.py-305-308 (1)

305-308: ⚠️ Potential issue | 🟡 Minor

Only return rows that were actually soft-deleted.

This loop appends every selected id even if soft_delete_message() returns False. A concurrent manual delete or another sweep can therefore emit duplicate message_delete events for the same message.

🧹 Proposed fix
-        for row in rows:
-            await self.soft_delete_message(row[0])
-            ids.append((row[0], row[1]))
+        for message_id, channel_id in rows:
+            if await self.soft_delete_message(message_id):
+                ids.append((message_id, channel_id))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tinyagentos/chat/message_store.py` around lines 305 - 308, The loop currently
appends every selected id to ids regardless of the result of
soft_delete_message, causing duplicate events; update the loop in
message_store.py so that after awaiting self.soft_delete_message(row[0]) you
check its boolean return and only append (row[0], row[1]) to ids when
soft_delete_message returned True (keep using the same rows variable and ids
list and call to soft_delete_message to locate the code).
desktop/src/apps/MessagesApp.tsx-175-180 (1)

175-180: ⚠️ Potential issue | 🟡 Minor

Don't collapse 30 days to 4w.

2592000 seconds becomes 4w here, which underreports the configured TTL by two days. The header badge should preserve exact supported presets like 30d.

🕒 Proposed fix
 function formatTTL(seconds: number): string {
-  if (seconds < 3600) return `${Math.round(seconds / 60)}m`;
-  if (seconds < 86400) return `${Math.round(seconds / 3600)}h`;
-  if (seconds < 604800 * 4) return `${Math.round(seconds / 86400)}d`;
-  return `${Math.round(seconds / 604800)}w`;
+  if (seconds % 86400 === 0) {
+    const days = seconds / 86400;
+    if (days % 7 === 0 && days < 30) return `${days / 7}w`;
+    return `${days}d`;
+  }
+  if (seconds % 3600 === 0) return `${seconds / 3600}h`;
+  return `${Math.round(seconds / 60)}m`;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/MessagesApp.tsx` around lines 175 - 180, formatTTL currently
collapses a 30-day TTL (2592000 seconds) into "4w"; update the formatTTL
function to special-case known preset values so they render exactly (e.g., if
seconds === 2592000 return "30d") before the existing range checks; reference
the formatTTL function and the 2592000 seconds value when making the change so
the header badge preserves supported presets like "30d".
docs/superpowers/plans/2026-04-20-chat-phase-2b-2b-ephemeral-polish.md-56-71 (1)

56-71: ⚠️ Potential issue | 🟡 Minor

Return type annotation is incorrect; method returns list[tuple], not list[str].

The docstring and signature say list[str], but line 69 appends (row[0], row[1]) tuples.

Proposed fix
-async def sweep_expired(self) -> list[str]:
-    """Soft-delete messages past their expires_at. Returns list of deleted ids."""
+async def sweep_expired(self) -> list[tuple[str, str]]:
+    """Soft-delete messages past their expires_at. Returns list of (message_id, channel_id) tuples."""
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-04-20-chat-phase-2b-2b-ephemeral-polish.md`
around lines 56 - 71, The return type annotation and docstring for sweep_expired
are incorrect: the function actually returns a list of (id, channel_id) tuples.
Update the async def sweep_expired signature to return list[tuple[str, str]] (or
list[tuple[str, int]] if channel_id is numeric) and revise the docstring to
describe "Returns list of (id, channel_id) tuples" so the type and documentation
match the behavior invoked when appending (row[0], row[1]) and calling
soft_delete_message.
🧹 Nitpick comments (8)
docs/superpowers/specs/2026-04-18-agent-persona-and-memory-deploy-design.md (1)

280-281: Tighten wording in the “Open questions” preface.

“None blocking.” reads awkwardly. Consider “Nothing is blocking.” (or “No blocking items.”) for clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/specs/2026-04-18-agent-persona-and-memory-deploy-design.md`
around lines 280 - 281, Replace the awkward phrase "None blocking." in the "Open
questions" preface with a clearer alternative such as "Nothing is blocking." or
"No blocking items." — locate the preface containing the exact text "None
blocking." and update that sentence to one of the suggested phrasings to improve
clarity and tone.
docs/superpowers/plans/2026-04-16-agent-resources-advanced.md (1)

129-130: Use repo-relative commands instead of machine-specific absolute paths.

The hardcoded /Volumes/NVMe/Users/jay/... paths are not reproducible for other devs/CI. Prefer repository-relative commands.

Suggested doc tweak
-cd /Volumes/NVMe/Users/jay/Development/tinyagentos/desktop && npx tsc --noEmit 2>&1 | head -20
+cd desktop && npx tsc --noEmit 2>&1 | head -20
-cd /Volumes/NVMe/Users/jay/Development/tinyagentos/desktop && npm test 2>&1 | tail -8
+cd desktop && npm test 2>&1 | tail -8

Also applies to: 201-202, 395-396, 403-404

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-04-16-agent-resources-advanced.md` around lines
129 - 130, Replace the machine-specific hardcoded command string "cd
/Volumes/NVMe/Users/jay/Development/tinyagentos/desktop && npx tsc --noEmit 2>&1
| head -20" (and the other occurrences with the same absolute path) with a
repository-relative invocation; for example, change the CD step to use the repo
root (e.g., "cd .", "cd $(git rev-parse --show-toplevel)" or simply run "npx tsc
--noEmit 2>&1 | head -20" from repo-relative docs), ensuring all instances (the
shown command and the similar occurrences mentioned) use relative paths so the
docs/commands work on other developers' machines and CI.
docs/superpowers/specs/2026-04-18-framework-update-phase-1-design.md (1)

105-110: Add a language tag to the fenced code block.

This block is unlabeled; set it to text (or bash) to satisfy linting and improve rendering consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/specs/2026-04-18-framework-update-phase-1-design.md` around
lines 105 - 110, The fenced code block containing the sample tag pattern
starting with "pre-framework-update-<new-tag>-<utc-compact>" is unlabeled;
update the opening triple-backtick to include a language tag (e.g., "text" or
"bash") so it reads "```text" or "```bash" to satisfy the linter and ensure
consistent rendering of that example block.
tests/test_chat_threads.py (1)

243-253: Add status assertions for setup requests in the new endpoint test.

The new test currently assumes channel/message creation succeeded; if setup fails, later JSON indexing errors make the failure harder to diagnose.

🔧 Proposed fix
         ch_r = await client.post("/api/chat/channels",
             json={"name": "g", "type": "group", "description": "", "topic": "",
                   "members": ["user", "tom"], "created_by": "user"})
+        assert ch_r.status_code in (200, 201), ch_r.json()
         ch_id = ch_r.json()["id"]
         p1 = await client.post("/api/chat/messages",
             json={"channel_id": ch_id, "author_id": "user", "author_type": "user",
                   "content": "parent one", "content_type": "text"})
+        assert p1.status_code in (200, 201), p1.json()
         parent_id = p1.json()["id"]
         await client.post("/api/chat/messages",
             json={"channel_id": ch_id, "author_id": "user", "author_type": "user",
                   "content": "reply", "content_type": "text", "thread_id": parent_id})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_chat_threads.py` around lines 243 - 253, The test setup is missing
assertions on the responses from the POST calls so failures produce confusing
JSON errors; after each client.post to "/api/chat/channels" and
"/api/chat/messages" (responses held in ch_r and p1, and the subsequent message
post) add assertions that response.status_code == 200 (or the expected status)
before calling .json(), and optionally assert that ch_r.json().get("id") and
p1.json().get("id") are present to ensure ch_id and parent_id exist; this
ensures immediate, clear failures if channel or message creation fails.
desktop/src/apps/chat/ChannelSettingsPanel.tsx (1)

181-185: Capture previous TTL explicitly before optimistic update.

Using the closed-over ephemeralTtl works in simple cases, but capturing prev makes rollback intent deterministic and clearer under async timing.

🔧 Proposed fix
               onChange={(e) => {
+                const prev = ephemeralTtl;
                 const val = e.target.value === "" ? null : Number(e.target.value);
                 setEphemeralTtl(val);
-                apply({ ephemeral_ttl_seconds: val }, () => setEphemeralTtl(ephemeralTtl));
+                apply({ ephemeral_ttl_seconds: val }, () => setEphemeralTtl(prev));
               }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/apps/chat/ChannelSettingsPanel.tsx` around lines 181 - 185, The
onChange handler uses the closed-over ephemeralTtl for rollback; capture the
previous value into a local variable (e.g., const prev = ephemeralTtl) before
doing the optimistic update, then call setEphemeralTtl(val) and call apply({
ephemeral_ttl_seconds: val }, () => setEphemeralTtl(prev)) so the rollback uses
the captured prev value deterministically; update references in the
ChannelSettingsPanel onChange handler and ensure prev's type matches
ephemeralTtl (number | null).
docs/superpowers/plans/2026-04-20-chat-phase-2b-2b-ephemeral-polish.md (3)

30-35: Silent exception swallowing may hide real database errors.

The bare except Exception: pass will hide issues like database locks, disk full, or permission errors. Consider logging at debug level.

Proposed improvement
 try:
     await self._db.execute("ALTER TABLE chat_messages ADD COLUMN expires_at REAL")
     await self._db.commit()
-except Exception:
-    pass
+except Exception as e:
+    # Column likely already exists; log for visibility
+    import logging
+    logging.getLogger(__name__).debug("expires_at migration skipped: %s", e)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-04-20-chat-phase-2b-2b-ephemeral-polish.md`
around lines 30 - 35, The try/except block that runs self._db.execute("ALTER
TABLE chat_messages ADD COLUMN expires_at REAL") currently swallows all
exceptions; update the error handling to catch the exception, log it at debug
(or warning) with the exception details and context (e.g., include the SQL and
table name) instead of silently passing, and only suppress the error for
expected/benign cases (e.g., column already exists) by checking the exception
message/type; reference the block around self._db.execute and await
self._db.commit() to locate where to add logging and conditional suppression.

60-70: N+1 delete pattern could be slow with many expired messages.

The current approach selects all expired messages then deletes them one-by-one. For channels with heavy ephemeral traffic, consider a batch update or wrapping in a transaction for atomicity.

Alternative: batch soft-delete
async def sweep_expired(self) -> list[tuple[str, str]]:
    """Soft-delete messages past their expires_at. Returns list of (message_id, channel_id) tuples."""
    import time as _time
    now = _time.time()
    async with self._db.execute(
        "SELECT id, channel_id FROM chat_messages "
        "WHERE expires_at IS NOT NULL AND expires_at < ? AND deleted_at IS NULL",
        (now,),
    ) as cursor:
        rows = await cursor.fetchall()
    if not rows:
        return []
    ids = [(row[0], row[1]) for row in rows]
    # Batch update instead of N individual calls
    await self._db.execute(
        "UPDATE chat_messages SET deleted_at = ? WHERE id IN (%s)" % ",".join("?" * len(ids)),
        (now, *[mid for mid, _ in ids]),
    )
    await self._db.commit()
    return ids
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-04-20-chat-phase-2b-2b-ephemeral-polish.md`
around lines 60 - 70, The sweep_expired implementation currently selects expired
rows and calls self.soft_delete_message for each row (N+1 pattern); change it to
collect ids from the SELECT, then perform a single batch UPDATE on chat_messages
setting deleted_at = now WHERE id IN (...) using self._db.execute with
parameterized placeholders (and call await self._db.commit()), return the list
of (id, channel_id) tuples, and/or wrap the SELECT+UPDATE in a transaction to
ensure atomicity; remove per-row calls to soft_delete_message or adjust
soft_delete_message to operate on batches if it contains additional side
effects.

228-247: SELECT parent.* with GROUP BY is non-standard SQL.

SQLite allows SELECT * with GROUP BY, but the values of non-aggregated columns are implementation-defined. Explicitly listing columns makes the query portable and its behavior explicit.

Explicit column selection
-           SELECT
-             parent.*,
+           SELECT
+             parent.id,
+             parent.channel_id,
+             parent.author_id,
+             parent.content,
+             parent.created_at,
              COUNT(reply.id) AS reply_count,
              MAX(reply.created_at) AS last_reply_at
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-04-20-chat-phase-2b-2b-ephemeral-polish.md`
around lines 228 - 247, The SQL uses "SELECT parent.*" with a GROUP BY which is
non-portable; replace "SELECT parent.*" with an explicit list of the
chat_messages parent columns used by the code (e.g., id, thread_id, channel_id,
user_id, content, created_at, updated_at, deleted_at — whatever columns exist
and _parse expects) and then either include those non-aggregated columns in the
GROUP BY or convert them to aggregates as appropriate so the query returns
deterministic values; update the query string passed to self._db.execute (the
block that binds (channel_id,)) and keep using cursor.description, rows, and
_parse as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@desktop/src/apps/chat/AllThreadsList.tsx`:
- Around line 37-48: The effect in AllThreadsList can let an earlier fetch
resolve after channelId changes and call setThreads for the wrong channel;
modify the useEffect to create an AbortController for each fetch, pass
controller.signal into fetch(`/api/chat/channels/${channelId}/threads`, { signal
}), and call controller.abort() in the cleanup to cancel stale requests; also
adjust the .catch to ignore abort errors (check for e.name === 'AbortError' or e
instanceof DOMException) so aborted requests don't call setError, and only
update state (setThreads/setError/setLoading) from non-aborted responses.

In `@desktop/src/apps/chat/HelpPanel.tsx`:
- Around line 50-54: The help panel currently renders the guide source verbatim
using a <pre> wrapper when `!error && markdown !== null`; change this to render
parsed Markdown instead by replacing the raw <pre> output with a
Markdown-to-JSX/HTML renderer (e.g., react-markdown or your app's
MarkdownRenderer component) so headings, lists and links are properly structured
and clickable; keep or map existing styling (the className used on the <pre>) to
the renderer, ensure any HTML is sanitized or use a safe renderer option, and
update the conditional render in HelpPanel (the block that checks `!error &&
markdown !== null`) to render the markdown component fed with the `markdown`
string.

In `@desktop/src/apps/MessagesApp.tsx`:
- Around line 1307-1315: The PATCH handler for channel updates in
tinyagentos/routes/chat.py is not persisting ephemeral_ttl_seconds even though
ChannelSettingsPanel.tsx sends it and MessagesApp.tsx reads it; update the PATCH
/api/chat/channels/{channel_id} handler (the channel update function) to accept
ephemeral_ttl_seconds from the request payload, validate it, set
channel.settings['ephemeral_ttl_seconds'] (or the Channel model's
ephemeral_ttl_seconds field) accordingly, and save the channel persistently
before returning the updated channel JSON so the TTL chosen in
ChannelSettingsPanel.tsx survives reloads.

In `@docs/superpowers/plans/2026-04-16-agent-resources-advanced.md`:
- Around line 1-430: The plan file
docs/superpowers/plans/2026-04-16-agent-resources-advanced.md appears unrelated
to this chat Phase 2b PR; remove it from this PR by either moving the file to
the correct feature branch/PR or reverting the addition in this branch so the
chat-only PR stays focused. If this plan was meant here, update the PR
description to explicitly include the AgentsApp.tsx changes and reference key
symbols (AgentsApp.tsx, MEMORY_STEPS_MB, state vars
showAdvanced/advancedLoaded/systemRamMb/systemCpuCores) so reviewers can
validate scope; otherwise stage a git move/checkout to relocate the file and
commit to the appropriate agents/resources PR.

In `@docs/superpowers/plans/2026-04-16-guided-add-provider.md`:
- Around line 197-204: The ProviderCreate schema in the plan is out of sync with
the API model; update the plan's ProviderCreate to match the current contract
used by the route handler (tinyagentos/routes/providers.py) by replacing
name/priority/api_key_secret with the exact fields and types used there (e.g.,
display_name, api_key, auth_token, custom_headers, model, and any
optional/defaults or typing like Optional[str] or Dict[str,str]); ensure field
names, optionality, and defaults mirror the route's ProviderCreate/validation
logic and update any references in the plan to use the same symbol names so
add_provider handling remains compatible.
- Around line 434-451: The code currently swallows failures when calling
fetch("/api/secrets") and proceeds to save the provider, which can leave a
provider without credentials; update the secret-save logic around the fetch call
so failures are treated as blocking: check the fetch response.ok (or catch
network errors) and if not successful throw or return an error to the caller (do
not set apiKeySecret unless the request succeeded), and surface that error to
the UI (or require an explicit user confirmation before proceeding).
Specifically modify the block that builds secretName and calls
fetch("/api/secrets") so that non-2xx responses and exceptions cause early
exit/error (and avoid continuing to provider creation when apiKeySecret is
undefined).
- Around line 818-820: The plan contains a plaintext credential ("SSH:
jay@192.168.6.123 (password: alexander04)") — remove that secret immediately,
replace it with a redacted placeholder (e.g., "SSH: user@host (password:
REDACTED)" or instructions to use an SSH key/secrets manager), and rotate the
exposed password on the affected account; then purge the secret from Git history
using a history-rewrite tool (git filter-repo or BFG) and force-push the cleaned
branch, and notify stakeholders of the rotation. Ensure the change is applied to
the snippet that includes the SSH line and the command block "cd
/home/jay/tinyagentos && git pull origin feat/guided-add-provider && sudo
systemctl restart tinyagentos" so no plaintext credentials remain in current
files or history.

In `@docs/superpowers/plans/2026-04-18-agent-persona-and-memory-deploy.md`:
- Around line 1-13: The plan titled "Agent persona + memory deploy —
implementation plan" was added into a PR focused on "Chat Phase 2b polish",
which is out-of-scope; move this entire plan out of the current PR into its own
dedicated branch/PR (e.g., the feature branch referenced in the text) or convert
it into a clearly marked follow-up artifact by removing it from the change set
and adding a short cross-reference note in this PR that links to the separate
plan PR/issue; ensure the PR only contains Chat Phase 2b polish changes and that
the persona/memory spec reference is preserved in the separate work item for
reviewers.
- Around line 796-814: The DeployAgentRequest Pydantic model uses a mutable
default for fallback_models (fallback_models: list[str] = []), which should be
replaced with a per-instance factory; update the field to use Pydantic's
Field(default_factory=list) and ensure Field is imported, e.g., change the
fallback_models declaration in the DeployAgentRequest class to use
Field(default_factory=list) so each instance gets its own list.
- Around line 575-590: Decide and enforce a single archive API contract (sync or
async) and make all calls consistent: choose whether archive.record /
archive.query are coroutine functions; if you opt for async, change the caller
functions (e.g., the block using self.archive.record, all dual-write tasks and
smoke-checks that call archive.record/archive.query) to async defs, prepend
await to every archive.record/archive.query call, and use try/except with
logger.exception inside async context; if you opt for sync, remove all awaits
and ensure archive is a normal object with synchronous record/query methods and
update type hints and tests accordingly; update any references to
self.archive.record, archive.record, and archive.query across Task 2.1–2.4 to
match the chosen interface.

In `@docs/superpowers/plans/2026-04-18-framework-update-phase-1.md`:
- Around line 1-13: The file "Framework update — Phase 1 Implementation Plan"
belongs to a separate initiative and should be removed from this PR: either
delete or revert this file from the current branch and create a dedicated
branch/PR (e.g., feat/framework-update-phase-1) that contains the plan plus
related artifacts (references to framework_update.py,
docs/superpowers/specs/2026-04-18-framework-update-phase-1-design.md, and any
LXC/container helpers) or explicitly add a README link tying this PR to that
initiative; ensure the current PR only contains chat Phase 2b polish changes and
update the PR description to reference the separate framework-update PR if you
intend to keep the plan linked.
- Around line 859-873: The test test_bootstrap_sets_last_seen_at is too
permissive by accepting 401/403; change it so the authenticated path is
exercised and its observable effect is asserted: ensure the request uses a valid
auth setup (or the test fixture provides authenticated headers) so
resp.status_code == 200 and then assert agent["bootstrap_last_seen_at"] is not
None; alternatively split into two assertions/tests: one that sends no/invalid
auth and expects 401/403 and verifies bootstrap_last_seen_at stays None, and a
separate authenticated test that calls GET /api/openclaw/bootstrap?agent=atlas
(using the same app.state.config.agents entry) and asserts resp.status_code ==
200 and that the agent record was updated.

In `@docs/superpowers/specs/2026-04-18-framework-update-phase-1-design.md`:
- Around line 56-57: The spec and implementation are out of sync: although the
spec defines service_name (used by systemctl stop/start) and install_script
(invoked via exec_in_container), the runner and install script still infer the
systemd unit from the framework ID; update the update path to pass the explicit
service_name through to exec_in_container and ensure the install script uses
that parameter for systemctl stop/start instead of deriving from framework ID.
Concretely, modify the runner code that calls exec_in_container to include the
service_name field from the update payload, and update the install_script
invocation/argument handling so the script reads service_name and uses it in the
systemctl stop/start commands (replace any logic that maps framework ID → unit
name).
- Around line 157-161: The timeout logic uses a strict > comparison against a
second-resolution deadline which can misclassify reconnects that land exactly on
the boundary; update the timeout check inside _wait_for_bootstrap_ping to use >=
when comparing the current timestamp to the deadline (so equality counts as
timed-out/handled appropriately), and make the same change to the other
occurrence referenced (the check around line 175) so both deadline comparisons
use >=; reference the functions/variables _wait_for_bootstrap_ping, started_at,
deadline, and _mark_failed when locating the checks to change.

In `@tinyagentos/app.py`:
- Around line 327-347: The ephemeral sweep task started with
asyncio.create_task(_ephemeral_sweep_loop(app)) is never tracked or cancelled,
causing a race on app.state.chat_messages and app.state.chat_hub during
shutdown; modify the lifespan logic to save the task reference (e.g., assign the
result of asyncio.create_task to app.state._ephemeral_sweep_task), ensure
_ephemeral_sweep_loop checks for cancellation (it can rely on
asyncio.CancelledError), and in the teardown after the yield cancel and await
the task (call app.state._ephemeral_sweep_task.cancel() and await it, handling
CancelledError) before closing stores so the loop cannot access closed
resources.

In `@tinyagentos/chat/message_store.py`:
- Around line 84-88: The migration currently swallows all exceptions when
running self._db.execute("ALTER TABLE chat_messages ADD COLUMN expires_at
REAL"), which can hide real DB errors; change the except block to catch the
exception as e, detect SQLite's duplicate-column case (e.g.,
sqlite3.OperationalError with message containing "duplicate column" or "already
exists") and silently ignore only that, but re-raise any other exceptions so
corrupt/locked DB errors surface; ensure sqlite3 is imported and reference the
ALTER TABLE statement, self._db.execute, and the exception variable (e) in your
fix.

---

Outside diff comments:
In `@desktop/src/apps/chat/AttachmentLightbox.tsx`:
- Around line 70-75: The dialog overlay in AttachmentLightbox.tsx (the div with
role="dialog" and handlers onClose and handleWheel) is missing
aria-modal="true"; update that element to include aria-modal="true" so assistive
tech treats it as a blocking modal while the lightbox is open. Ensure the
attribute is added alongside role="dialog" and aria-label="Image viewer" on the
same div.

In `@tinyagentos/routes/chat.py`:
- Around line 76-97: The TTL/expiry computation for ephemeral channels is
duplicated and missing on some code paths (websocket message branch, the /help
system-message path and the call from agent_chat_router to
msg_store.send_message), so extract the logic that reads channel settings and
computes expires_at into a shared helper (e.g.,
compute_expires_at(channel_store, channel_id) or
ChannelExpiryService.get_expires_at) that calls get_channel and returns the
proper expires_at or None; then call that helper from the websocket message
handler (where _ws_ttl/_ws_expires_at are currently computed), from the /help
system-message path, and from the code that invokes send_message in
agent_chat_router (so every call to send_message uses the helper to pass
expires_at consistently).

---

Minor comments:
In `@desktop/src/apps/MessagesApp.tsx`:
- Around line 175-180: formatTTL currently collapses a 30-day TTL (2592000
seconds) into "4w"; update the formatTTL function to special-case known preset
values so they render exactly (e.g., if seconds === 2592000 return "30d") before
the existing range checks; reference the formatTTL function and the 2592000
seconds value when making the change so the header badge preserves supported
presets like "30d".

In `@docs/superpowers/plans/2026-04-18-framework-update-phase-1.md`:
- Around line 1650-1653: The build step uses a machine-specific absolute path
("cd /Volumes/NVMe/Users/jay/Development/tinyagentos") which breaks portability;
change it to a repository-relative command or remove the explicit cd and run
from the repo root (e.g., use repo root resolution like git rev-parse
--show-toplevel or relative paths) so CI/devs can run the script reliably, and
keep the subsequent git add lines referencing "static/desktop" and
"desktop/tsconfig.tsbuildinfo" as relative paths.
- Around line 1380-1413: The component FrameworkTab receives onUpdated but never
calls it; modify the doUpdate flow so the parent is notified when an update is
successfully initiated: inside the doUpdate function (after awaiting
startFrameworkUpdate(agent.name) and successfully reloading via load()), call
onUpdated() (ensure it's invoked only on success, e.g., inside the try block
after await load()); this uses existing symbols FrameworkTab, doUpdate, load,
and startFrameworkUpdate to locate where to add the call.

In `@docs/superpowers/plans/2026-04-20-chat-phase-2b-2b-ephemeral-polish.md`:
- Around line 56-71: The return type annotation and docstring for sweep_expired
are incorrect: the function actually returns a list of (id, channel_id) tuples.
Update the async def sweep_expired signature to return list[tuple[str, str]] (or
list[tuple[str, int]] if channel_id is numeric) and revise the docstring to
describe "Returns list of (id, channel_id) tuples" so the type and documentation
match the behavior invoked when appending (row[0], row[1]) and calling
soft_delete_message.

In `@docs/superpowers/specs/2026-04-18-framework-update-phase-1-design.md`:
- Line 54: Update the scheme description for the release_source field to use
canonical product capitalization: change the example scheme from
"github:<owner>/<repo>" to "GitHub:<owner>/<repo>" so the spec consistently uses
"GitHub" everywhere (locate the table row for `release_source` and edit the
string in that cell).

In `@tests/test_chat_threads.py`:
- Line 241: The test unpacks an unused app from the coroutine result of
_authed_thread_client, causing lint noise; change the unpack to discard the
unused binding (e.g., replace "app, client = await
_authed_thread_client(tmp_path)" with "_, client = await
_authed_thread_client(tmp_path)") so only client is used while keeping the call
to _authed_thread_client for setup.

In `@tinyagentos/chat/message_store.py`:
- Around line 305-308: The loop currently appends every selected id to ids
regardless of the result of soft_delete_message, causing duplicate events;
update the loop in message_store.py so that after awaiting
self.soft_delete_message(row[0]) you check its boolean return and only append
(row[0], row[1]) to ids when soft_delete_message returned True (keep using the
same rows variable and ids list and call to soft_delete_message to locate the
code).

---

Nitpick comments:
In `@desktop/src/apps/chat/ChannelSettingsPanel.tsx`:
- Around line 181-185: The onChange handler uses the closed-over ephemeralTtl
for rollback; capture the previous value into a local variable (e.g., const prev
= ephemeralTtl) before doing the optimistic update, then call
setEphemeralTtl(val) and call apply({ ephemeral_ttl_seconds: val }, () =>
setEphemeralTtl(prev)) so the rollback uses the captured prev value
deterministically; update references in the ChannelSettingsPanel onChange
handler and ensure prev's type matches ephemeralTtl (number | null).

In `@docs/superpowers/plans/2026-04-16-agent-resources-advanced.md`:
- Around line 129-130: Replace the machine-specific hardcoded command string "cd
/Volumes/NVMe/Users/jay/Development/tinyagentos/desktop && npx tsc --noEmit 2>&1
| head -20" (and the other occurrences with the same absolute path) with a
repository-relative invocation; for example, change the CD step to use the repo
root (e.g., "cd .", "cd $(git rev-parse --show-toplevel)" or simply run "npx tsc
--noEmit 2>&1 | head -20" from repo-relative docs), ensuring all instances (the
shown command and the similar occurrences mentioned) use relative paths so the
docs/commands work on other developers' machines and CI.

In `@docs/superpowers/plans/2026-04-20-chat-phase-2b-2b-ephemeral-polish.md`:
- Around line 30-35: The try/except block that runs self._db.execute("ALTER
TABLE chat_messages ADD COLUMN expires_at REAL") currently swallows all
exceptions; update the error handling to catch the exception, log it at debug
(or warning) with the exception details and context (e.g., include the SQL and
table name) instead of silently passing, and only suppress the error for
expected/benign cases (e.g., column already exists) by checking the exception
message/type; reference the block around self._db.execute and await
self._db.commit() to locate where to add logging and conditional suppression.
- Around line 60-70: The sweep_expired implementation currently selects expired
rows and calls self.soft_delete_message for each row (N+1 pattern); change it to
collect ids from the SELECT, then perform a single batch UPDATE on chat_messages
setting deleted_at = now WHERE id IN (...) using self._db.execute with
parameterized placeholders (and call await self._db.commit()), return the list
of (id, channel_id) tuples, and/or wrap the SELECT+UPDATE in a transaction to
ensure atomicity; remove per-row calls to soft_delete_message or adjust
soft_delete_message to operate on batches if it contains additional side
effects.
- Around line 228-247: The SQL uses "SELECT parent.*" with a GROUP BY which is
non-portable; replace "SELECT parent.*" with an explicit list of the
chat_messages parent columns used by the code (e.g., id, thread_id, channel_id,
user_id, content, created_at, updated_at, deleted_at — whatever columns exist
and _parse expects) and then either include those non-aggregated columns in the
GROUP BY or convert them to aggregates as appropriate so the query returns
deterministic values; update the query string passed to self._db.execute (the
block that binds (channel_id,)) and keep using cursor.description, rows, and
_parse as before.

In `@docs/superpowers/specs/2026-04-18-agent-persona-and-memory-deploy-design.md`:
- Around line 280-281: Replace the awkward phrase "None blocking." in the "Open
questions" preface with a clearer alternative such as "Nothing is blocking." or
"No blocking items." — locate the preface containing the exact text "None
blocking." and update that sentence to one of the suggested phrasings to improve
clarity and tone.

In `@docs/superpowers/specs/2026-04-18-framework-update-phase-1-design.md`:
- Around line 105-110: The fenced code block containing the sample tag pattern
starting with "pre-framework-update-<new-tag>-<utc-compact>" is unlabeled;
update the opening triple-backtick to include a language tag (e.g., "text" or
"bash") so it reads "```text" or "```bash" to satisfy the linter and ensure
consistent rendering of that example block.

In `@tests/test_chat_threads.py`:
- Around line 243-253: The test setup is missing assertions on the responses
from the POST calls so failures produce confusing JSON errors; after each
client.post to "/api/chat/channels" and "/api/chat/messages" (responses held in
ch_r and p1, and the subsequent message post) add assertions that
response.status_code == 200 (or the expected status) before calling .json(), and
optionally assert that ch_r.json().get("id") and p1.json().get("id") are present
to ensure ch_id and parent_id exist; this ensures immediate, clear failures if
channel or message creation fails.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: be7ace5a-5fdf-47a2-9fd3-3b4c92074663

📥 Commits

Reviewing files that changed from the base of the PR and between 3ba7668 and d079378.

📒 Files selected for processing (36)
  • desktop/src/apps/MessagesApp.tsx
  • desktop/src/apps/chat/AllThreadsList.tsx
  • desktop/src/apps/chat/AttachmentLightbox.tsx
  • desktop/src/apps/chat/ChannelSettingsPanel.tsx
  • desktop/src/apps/chat/HelpPanel.tsx
  • desktop/src/apps/chat/__tests__/AllThreadsList.test.tsx
  • desktop/src/apps/chat/__tests__/HelpPanel.test.tsx
  • desktop/src/lib/channel-admin-api.ts
  • desktop/tsconfig.tsbuildinfo
  • docs/superpowers/plans/2026-04-16-agent-resources-advanced.md
  • docs/superpowers/plans/2026-04-16-guided-add-provider.md
  • docs/superpowers/plans/2026-04-18-agent-persona-and-memory-deploy.md
  • docs/superpowers/plans/2026-04-18-framework-update-phase-1.md
  • docs/superpowers/plans/2026-04-20-chat-phase-2b-2b-ephemeral-polish.md
  • docs/superpowers/specs/2026-04-18-agent-persona-and-memory-deploy-design.md
  • docs/superpowers/specs/2026-04-18-framework-update-phase-1-design.md
  • docs/superpowers/specs/2026-04-20-chat-phase-2b-2b-ephemeral-polish-design.md
  • static/desktop/assets/MCPApp-Bf_LfaaM.js
  • static/desktop/assets/MessagesApp-CkhTvSE9.js
  • static/desktop/assets/MessagesApp-D99HV6_a.js
  • static/desktop/assets/ProvidersApp-BMhQW6pS.js
  • static/desktop/assets/SettingsApp-DtCl5xI-.js
  • static/desktop/assets/chat-nY3Fd9ku.js
  • static/desktop/assets/main-ClVzsOcM.js
  • static/desktop/assets/tokens-C0Jn8tG6.js
  • static/desktop/assets/tokens-CX3OOjWL.css
  • static/desktop/assets/tokens-DbWedNtZ.css
  • static/desktop/chat.html
  • static/desktop/index.html
  • tests/e2e/test_chat_phase2b2b.py
  • tests/test_chat_docs.py
  • tests/test_chat_ephemeral.py
  • tests/test_chat_threads.py
  • tinyagentos/app.py
  • tinyagentos/chat/message_store.py
  • tinyagentos/routes/chat.py
💤 Files with no reviewable changes (2)
  • static/desktop/assets/MessagesApp-CkhTvSE9.js
  • static/desktop/assets/tokens-DbWedNtZ.css

Comment thread desktop/src/apps/chat/AllThreadsList.tsx
Comment thread desktop/src/apps/chat/HelpPanel.tsx Outdated
Comment thread desktop/src/apps/MessagesApp.tsx
Comment thread docs/superpowers/plans/2026-04-16-agent-resources-advanced.md Outdated
Comment thread docs/superpowers/plans/2026-04-16-guided-add-provider.md Outdated
Comment thread docs/superpowers/plans/2026-04-18-framework-update-phase-1.md Outdated
Comment on lines +56 to +57
| `install_script` | Path of the script invoked via `exec_in_container` inside the agent's container. |
| `service_name` | Passed to `systemctl stop/start` by the install script. |
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

service_name is specified but not actually wired through the update path.

The spec says service_name drives systemctl stop/start, but the runner/script currently infer service from framework ID. That breaks multi-framework compatibility when service unit names diverge.

Proposed spec-level fix
-# Usage: taos-framework-update <framework> <tag> <asset_url>
+# Usage: taos-framework-update <framework> <service_name> <tag> <asset_url>

-FRAMEWORK="${1:?framework name required}"
-TAG="${2:?tag required}"
-URL="${3:?asset url required}"
+FRAMEWORK="${1:?framework name required}"
+SERVICE_NAME="${2:?service name required}"
+TAG="${3:?tag required}"
+URL="${4:?asset url required}"

-log "stopping ${FRAMEWORK}.service"
-systemctl stop "${FRAMEWORK}.service" || true
+log "stopping ${SERVICE_NAME}.service"
+systemctl stop "${SERVICE_NAME}.service" || true

-log "starting ${FRAMEWORK}.service"
-systemctl start "${FRAMEWORK}.service"
+log "starting ${SERVICE_NAME}.service"
+systemctl start "${SERVICE_NAME}.service"
 rc, stderr = await exec_in_container(container, [
     manifest["install_script"],
     manifest["id"],
+    manifest["service_name"],
     latest["tag"],
     latest["asset_url"],
 ], timeout=120)

Also applies to: 146-151, 195-226

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/specs/2026-04-18-framework-update-phase-1-design.md` around
lines 56 - 57, The spec and implementation are out of sync: although the spec
defines service_name (used by systemctl stop/start) and install_script (invoked
via exec_in_container), the runner and install script still infer the systemd
unit from the framework ID; update the update path to pass the explicit
service_name through to exec_in_container and ensure the install script uses
that parameter for systemctl stop/start instead of deriving from framework ID.
Concretely, modify the runner code that calls exec_in_container to include the
service_name field from the update payload, and update the install_script
invocation/argument handling so the script reads service_name and uses it in the
systemctl stop/start commands (replace any logic that maps framework ID → unit
name).

Comment on lines +157 to +161
# Wait for the bridge to call bootstrap, bounded by the 120s window from
# started_at (NOT from now — the script itself may have taken most of it).
deadline = started_at + 120
if not await _wait_for_bootstrap_ping(agent, deadline):
return await _mark_failed(agent, "bridge did not reconnect within 120s", snapshot=snap)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Bootstrap readiness check has a same-second false-timeout edge case.

Using strict > with second-level timestamps can miss a valid reconnect if bootstrap lands in the same second as started_at. Use >= (or compare against a pre-update baseline) to avoid spurious failures.

Also applies to: 175-175

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/specs/2026-04-18-framework-update-phase-1-design.md` around
lines 157 - 161, The timeout logic uses a strict > comparison against a
second-resolution deadline which can misclassify reconnects that land exactly on
the boundary; update the timeout check inside _wait_for_bootstrap_ping to use >=
when comparing the current timestamp to the deadline (so equality counts as
timed-out/handled appropriately), and make the same change to the other
occurrence referenced (the check around line 175) so both deadline comparisons
use >=; reference the functions/variables _wait_for_bootstrap_ping, started_at,
deadline, and _mark_failed when locating the checks to change.

Comment thread tinyagentos/app.py
Comment on lines +327 to +347
async def _ephemeral_sweep_loop(app: FastAPI) -> None:
import asyncio as _asyncio
_store = app.state.chat_messages
_hub = getattr(app.state, "chat_hub", None)
while True:
try:
deleted = await _store.sweep_expired()
if _hub is not None:
for mid, cid in deleted:
await _hub.broadcast(cid, {
"type": "message_delete",
"seq": _hub.next_seq(),
"channel_id": cid,
"message_id": mid,
"deleted_at": __import__("time").time(),
})
except Exception as _e:
logger.warning("ephemeral sweep failed: %s", _e)
await _asyncio.sleep(300)

asyncio.create_task(_ephemeral_sweep_loop(app))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== sweep loop task creation =="
rg -n "_ephemeral_sweep_loop|create_task\\(" tinyagentos/app.py

echo
echo "== explicit cancellation/await sites =="
rg -n "cancel\\(|CancelledError|await .*ephemeral" tinyagentos/app.py

Repository: jaylfc/tinyagentos

Length of output: 392


🏁 Script executed:

# Inspect the lifespan context manager structure around the code
sed -n '310,360p' tinyagentos/app.py | cat -n

Repository: jaylfc/tinyagentos

Length of output: 2910


🏁 Script executed:

# Check if other long-running tasks are managed similarly
rg -B 5 -A 10 "create_task.*(_probe_framework_versions|_ensure_agent_image_present)" tinyagentos/app.py

Repository: jaylfc/tinyagentos

Length of output: 1597


🏁 Script executed:

# Look for any task tracking or shutdown logic
rg -n "app\.state\.|tasks|shutdown|lifespan" tinyagentos/app.py | head -30

Repository: jaylfc/tinyagentos

Length of output: 1750


🏁 Script executed:

# Get the full lifespan function
rg -A 200 "async def lifespan\\(app: FastAPI\\)" tinyagentos/app.py | head -250

Repository: jaylfc/tinyagentos

Length of output: 10083


🏁 Script executed:

# Get lines after 440 to find the yield and teardown
sed -n '440,520p' tinyagentos/app.py | cat -n

Repository: jaylfc/tinyagentos

Length of output: 4539


🏁 Script executed:

# Find the yield statement in the lifespan function
rg -n "yield" tinyagentos/app.py | grep -A 5 -B 5 "241"

Repository: jaylfc/tinyagentos

Length of output: 44


🏁 Script executed:

# Get a broader view to find where yield is
sed -n '500,600p' tinyagentos/app.py | cat -n

Repository: jaylfc/tinyagentos

Length of output: 5768


🏁 Script executed:

# Get the teardown section after yield
sed -n '597,650p' tinyagentos/app.py | cat -n

Repository: jaylfc/tinyagentos

Length of output: 2575


Track and cancel the ephemeral sweep task during lifespan shutdown.

Line 347 creates a long-running task that is never tracked or cancelled. During shutdown (after yield at line 597), stores like chat_messages are closed at line 634, but the task's infinite loop may still be accessing them via app.state.chat_messages and app.state.chat_hub. This creates a race condition where the task could attempt to use closed resources.

Store the task reference and cancel it in the teardown section:

🔧 Proposed fix
-from contextlib import asynccontextmanager
+from contextlib import asynccontextmanager, suppress
@@
-        asyncio.create_task(_ephemeral_sweep_loop(app))
+        ephemeral_sweep_task = asyncio.create_task(_ephemeral_sweep_loop(app))
@@
-        yield
+        yield
+        ephemeral_sweep_task.cancel()
+        with suppress(asyncio.CancelledError):
+            await ephemeral_sweep_task
🧰 Tools
🪛 Ruff (0.15.10)

[warning] 343-343: Do not catch blind exception: Exception

(BLE001)


[warning] 347-347: Store a reference to the return value of asyncio.create_task

(RUF006)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tinyagentos/app.py` around lines 327 - 347, The ephemeral sweep task started
with asyncio.create_task(_ephemeral_sweep_loop(app)) is never tracked or
cancelled, causing a race on app.state.chat_messages and app.state.chat_hub
during shutdown; modify the lifespan logic to save the task reference (e.g.,
assign the result of asyncio.create_task to app.state._ephemeral_sweep_task),
ensure _ephemeral_sweep_loop checks for cancellation (it can rely on
asyncio.CancelledError), and in the teardown after the yield cancel and await
the task (call app.state._ephemeral_sweep_task.cancel() and await it, handling
CancelledError) before closing stores so the loop cannot access closed
resources.

Comment on lines +84 to +88
try:
await self._db.execute("ALTER TABLE chat_messages ADD COLUMN expires_at REAL")
await self._db.commit()
except Exception:
pass
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't swallow non-duplicate migration failures.

Catching every exception here and continuing means a locked/corrupt DB can boot without the expires_at column, and the first insert using it will fail later at runtime. Only ignore SQLite's duplicate-column case and re-raise everything else.

🛠️ Proposed fix
         try:
             await self._db.execute("ALTER TABLE chat_messages ADD COLUMN expires_at REAL")
             await self._db.commit()
-        except Exception:
-            pass
+        except Exception as exc:
+            if "duplicate column name: expires_at" not in str(exc).lower():
+                raise
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
await self._db.execute("ALTER TABLE chat_messages ADD COLUMN expires_at REAL")
await self._db.commit()
except Exception:
pass
try:
await self._db.execute("ALTER TABLE chat_messages ADD COLUMN expires_at REAL")
await self._db.commit()
except Exception as exc:
if "duplicate column name: expires_at" not in str(exc).lower():
raise
🧰 Tools
🪛 Ruff (0.15.10)

[error] 87-88: try-except-pass detected, consider logging the exception

(S110)


[warning] 87-87: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tinyagentos/chat/message_store.py` around lines 84 - 88, The migration
currently swallows all exceptions when running self._db.execute("ALTER TABLE
chat_messages ADD COLUMN expires_at REAL"), which can hide real DB errors;
change the except block to catch the exception as e, detect SQLite's
duplicate-column case (e.g., sqlite3.OperationalError with message containing
"duplicate column" or "already exists") and silently ignore only that, but
re-raise any other exceptions so corrupt/locked DB errors surface; ensure
sqlite3 is imported and reference the ALTER TABLE statement, self._db.execute,
and the exception variable (e) in your fix.

- PATCH /api/chat/channels/{id} now accepts ephemeral_ttl_seconds (persist)
- channel_store: set_ephemeral_ttl with validation (<= 30 days, >= 0, null allowed)
- HelpPanel: render markdown as JSX (headers/lists/links/code/bold) instead of raw <pre>
- AllThreadsList: AbortController on fetch to prevent stale responses overwriting current channel
- remove 4 unrelated plans + 2 unrelated specs that were force-added from other branches
  (these belonged on their own PRs; includes an earlier credential leak that is now removed)
@jaylfc jaylfc merged commit f665d7b into master Apr 20, 2026
7 of 8 checks passed
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