Tech14 min read

Adding Web Research Suggestion Chips and Twilog Context Ingestion to Kana

IkesanContents

When I consulted “Kana” (my custom web-based AI assistant) about purchasing a 3D printer (the X2D), the topic kept resurfacing in subsequent casual conversations.
Whenever I asked for a status update, Kana replied as if she were personally tracking the sale. Rather than actively monitoring real-time prices with external tools, the model was simply pulling previous topics from recent conversation history and repeating them.

If there is genuine interest, having the assistant check prices when discounts arrive and notify me is useful.
However, having the assistant automatically scour the web for every casual topic mentioned in chat creates unwanted noise and API calls.

I decided to decouple detecting user interest from initiating actual investigation.
I placed interactive suggestion chips above the chat input box, triggering search and web scraping only when clicked. In addition, I set up an hourly ingestion pipeline for my own posts on Twilog (an archival service that stores personal X/Twitter posts in chronological order) to share context across suggestion detection and everyday chat.

On November 13, 2026, I deployed these changes to the persistent gateway environment on my Mac mini.
This builds directly on pairing small-talk updates with image generation.

System Architecture Overview

To provide context for how these components interact, here is how the custom setup is organized:

ComponentRole in the System
KanaThe custom web chat interface used across desktop and mobile browsers
PiThe persistent local gateway server running on a Mac mini, coordinating storage and APIs
QwenThe primary LLM handling everyday conversations and executing tools (search, scraping)
Luna (Codex)A classifier model (GPT-5.3-Luna via Codex) evaluating whether an inquiry needs research
TwilogAn archival service storing personal tweets/posts in chronological order
Suggestion ChipsClickable UI buttons above the input box that propose a specific research action

Preventing Purchase Inquiries from Becoming the Assistant’s Own Interests

In modern LLMs, context windows feed past turns back into the model on every prompt.
While reading recent conversation history is necessary, when a specific topic lingers in the context window, it easily leaks into subsequent replies. Kana was using her own previous comments as evidence that an ongoing topic was still relevant.

I was the one evaluating the purchase; Kana itself was not waiting for a discount.
I added system prompt instructions to both routine chat and casual check-ins to prevent user inquiries from turning into Kana’s personal hobbies or actions. This stops the model from carrying over answers that pretend it has been lingering in the same place doing the same task after hours have passed.

Past consultation logs remain preserved in the SQLite database.
When I ask follow-up questions, the assistant still needs those previous constraints to respond accurately. The goal was not to make the model forget the context, but to treat it as reference data only when relevant to the current query.

To avoid mixing simulated persona updates with actual external information retrieved via tools, I also instructed the model never to claim it is actively watching or researching news unless an actual tool execution took place.

Detecting Interest Without Triggering Automatic Searches

Previously, an external agent integration called “Dots” sent me an unsolicited notification flagging a typo in a GitHub commit.
I had not requested that review, nor had I mentioned it in chat, so it evidently originated from automated GitHub repository polling.

While discovering topics from connected services is an interesting concept, having an assistant constantly inspect minute git diffs rarely provides actionable everyday suggestions.
Twilog, on the other hand, reflects what I want, what I am working on, or where I am stuck. I chose to use conversation logs with Kana and my Twilog posts as inputs for detecting potential research topics.

On the backend, an interest classifier runs separately from conversational generation.
Using Luna (GPT-5.3-Luna via Codex), it evaluates whether my interest is high, whether the topic remains unresolved, and whether there is an immediate reason to verify information. The classifier is instructed to ignore passing mentions, quotes of third-party posts, and items already purchased.

The classifier does not receive the entire conversation history.
Instead, it receives up to the last 8 messages (maximum 16,000 characters), along with the status and rationale of existing candidates. It preserves raw user text to avoid distorting meaning, and outputs no candidate if the context is insufficient. Candidate generation relies strictly on the user’s explicit statements or posts, preventing Kana’s own speculative responses from generating new research items.

The classifier has no tool access and does not execute searches.
Its responsibility stops at producing candidate chips above the input box, such as “Check current sale price for X2D Combo.”

flowchart TD
    A["Conversation history with Kana"] --> C["Background interest & gap classifier (Luna)"]
    B["Twilog post data"] --> C
    C --> D["Display suggestion chips above input"]
    D --> E["User clicks suggestion chip"]
    E --> F["Save and accept investigation task"]
    F --> G["Qwen executes web search & page scrape"]
    G --> H["Display investigation results as reply"]

Instead of having the assistant repeatedly ask in chat (“Are you still curious about this?”, “Should I look it up?”), the options sit quietly as clickable chips.
Because the target and scope are predetermined on the button label, there is no need for secondary confirmations upon clicking.

Actions triggered from suggestions are strictly limited to a single web research pass.
Available tools during execution are restricted to search, page content retrieval, and memory lookup. Clicking a chip will never trigger file modifications, deployments, purchases, or calendar reservations.

Preventing Stale Suggestion Chips from Lingering

Having suggestion chips above the input box would become just as irritating as repetitive small talk if obsolete suggestions remained indefinitely.
Each chip includes a close button (”×”), which records the dismissed state to the server’s SQLite database. Dismissed chips do not reappear upon page reloads or connections from other browsers.

The lifespan is capped at a maximum of 24 hours from creation.
While initial prototyping used 48 hours, I reduced this to 24 hours since suggestions should reflect immediate context. Postponing display does not extend this expiration deadline. Existing candidates adhere to the same cap.

Manually dismissed chips and expired candidates suppress re-proposing the same topic for 7 days.
This prevents Kana from phrasing the same topic slightly differently to generate new chips and indefinitely renew expiration timers.

If subsequent conversation reveals an item has been purchased or is no longer needed, the candidate is marked as resolved and closed.
Between receiving a new user message and completing classification, existing chips are temporarily hidden to prevent users from clicking outdated prompts before context updates.

Accepting Clicked Actions Only Once

When a suggestion chip is clicked, the system uses the predefined investigation prompt stored on the server.
The backend validates that the candidate is still active, not deleted, and not already accepted by another connection, recording the execution through standard chat persistence.

If network connectivity drops before receipt confirmation arrives, the client retries using an identical submission ID.
This avoids registering duplicate tasks from multiple clicks or simultaneous browser tabs. Submissions with tampered prompt text are rejected if they deviate from the stored candidate template.

Drafted text and attachments already entered in the chat input remain untouched when clicking a suggestion chip.
If I was in the middle of typing a message, clicking a research chip does not overwrite my draft.

Using Twilog for Both Candidate Detection and General Chat

Twilog ingestion is decoupled from the suggestion classifier.
Beyond feeding research suggestions, recent posts provide helpful context about what I have been up to for normal conversation and proactive check-ins.

Using authenticated access, a background job fetches the latest page once per hour and stores up to 20 personal posts from the last 24 hours.
The system records publication timestamps, retrieval timestamps, and source URLs. Third-party posts and retweets are excluded, and image attachments are not processed.

When chatting, up to 6 posts (maximum 6,000 characters) are automatically injected into Qwen’s context window.
The model is instructed to reference them only when naturally relevant to the current dialogue, rather than forcibly steering conversations toward tweets or adopting my personal experiences as its own. Imperative sentences within tweets are explicitly barred from being interpreted as task instructions.

The suggestion classifier references the same stored posts.
Disabling the suggestion feature does not disrupt Twilog synchronization or chat references.

Network fetch failures do not trigger immediate retries; the system simply waits for the next hourly schedule.
If data has not successfully refreshed for more than 2 hours, it is withheld from the chat prompt. Hourly synchronization is set to run persistently, and initial retrieval and SQLite storage on the external HDD were verified on the live server.

Summary of Changes Deployed on November 13

CategoryPrimary ChangesTarget
Conversation ControlAdded prompt rules preventing purchase inquiries from becoming Kana’s own interestsSystem instructions
Candidate ClassificationEvaluates conversation history (last 8 messages) and Twilog posts to generate suggestion chipsClassifier (Luna)
Research ExecutionClicking chips triggers single-pass web search and page scraping via QwenQwen tool execution
State Management24-hour expiration, manual dismissal, 7-day topic suppression, duplicate execution guardSQLite DB
Context SyncIngests Twilog hourly, sharing recent 24-hour posts across conversation and classifierCron job & DB
Operational LimitsCapped classification to 24 runs per 24 hours and active candidates to 5Backend settings

Databases and daemon files were backed up before deploying changes to the Mac mini environment, preserving existing shared chat sessions and user configurations.

Verification on the Local Environment

Classification and UI interactions were tested using synthetic conversations in an isolated database separate from the primary chat store.
I confirmed that the classifier creates candidates for unresolved purchase factors, retires candidates when items are marked as bought, and ignores third-party quote posts.

In the browser, I verified persistence of dismissals across reloads, real-time reflection across two simultaneous browser windows, expiration hiding, and draft retention during chip clicks.
For post-click execution, I verified end-to-end flow: Qwen executing web searches via Pi, retrieving live pages with HTTP 200 status, and saving answers. Search success was verified by inspecting retrieved raw HTML and database records rather than relying solely on the assistant’s output text.

Twilog ingestion successfully parsed authenticated pages and populated the shared context store.
While Qwen successfully referenced synthetic posts in conversation, determining whether references feel natural and tuning frequency thresholds will require extended use.

Hourly updates are active and verified for initial runs.
Assessing whether suggestion frequency feels well-balanced over time and whether 7-day topic suppression behaves as expected will be evaluated during ongoing daily use.

November 13 Additions: Health Logging and Mobile UI Polish

Beyond suggestion chips and Twilog integration, I updated health tracking and mobile usability on the same day.
Alongside backend capabilities, I streamlined where daily logs are stored and improved navigation between features.

Logging Health Metrics Through Chat

When I report sleep, weight, meals, activities, or general physical condition to Kana, the assistant parses and records the metrics in the database.
There is no need to prepend “record this”; the system extracts actual events completed by the user, ignoring hypothetical statements, future plans, or third-party mentions.

Data is stored in the same SQLite database as conversations and task logs.
Each entry links back to the original message ID and retains an edit history. Reports entered from smartphones or desktop browsers sync to the same server database.

The healthcare dashboard allows viewing charts by date range and metric, as well as inspecting raw messages and corrections.
Data can also be exported as CSV and JSON. Meal tracking currently captures food items and portions; precise nutritional estimation is not yet implemented.

Days without logs are kept null rather than filled with zeroes, preventing unrecorded days from falsely appearing as zero sleep or zero meals. Undated reports are excluded from daily timeline graphs.

Aggregating Sleep by Wake-up Date and Separating Naps

Sleep often spans across midnight, though sleeping late can keep both bedtime and wake-up time on the same calendar day.
Rather than treating overnight sleep differently based on midnight transitions, each sleep entry stores exact start and end timestamps, and daily totals aggregate under the wake-up date.

Reported SleepDaily AggregationTimeline Chart Representation
Nov 12, 23:00 to Nov 13, 07:00Nov 13 Primary SleepBar split between Nov 12 night and Nov 13 morning
Nov 13, 02:00 to Nov 13, 08:00Nov 13 Primary SleepSingle bar within Nov 13
Nov 13, 14:00 to Nov 13, 14:30 (Nap)Nov 13 NapDistinct color from primary sleep

“Haven’t slept for the night yet” and “took a nap” are recorded independently.
Naps do not overwrite primary sleep, and missing primary sleep logs are not assumed to be zero hours. Naps with unknown durations record the event without adding unverified hours to the daily total.

Statements reporting only duration (e.g., “slept 6 hours”) are saved as given.
Without start and end times, the system avoids generating synthetic chart intervals. Durations calculated from bedtime and wake-up timestamps remain distinct from biometric measurements.

Midnight Full Database Backups

Because healthcare metrics now reside alongside conversation logs, database loss carries higher consequences.
I scheduled a full backup of the entire SQLite database to a dedicated folder on an external HDD once daily at 00:30 JST, using full snapshots rather than incremental diffs.

Active databases are copied using SQLite’s online backup API, verified for file integrity, and finalized with a timestamped filename.
This logic is integrated into Kana’s existing daemon without adding separate cron utilities. If the machine was asleep or offline at 00:30, it creates a single snapshot upon waking rather than firing all missed instances at once.

Backups currently reside on the same external HDD, so transfers to separate media or remote NAS remain future steps.
Backups cover the SQLite database file itself, not raw attachment directories for images or audio.

Integrity of the initial backup snapshot and duplicate prevention for the same date were verified.
Automated execution at the scheduled midnight time will be confirmed during normal overnight operation.

Consolidating Navigation into a Shared Sidebar

As features expanded, navigation links accumulated at the top of the chat view, consuming screen real estate on mobile devices and causing header heights to differ between pages.
When launched as a standalone Progressive Web App (PWA) from the home screen, opening sub-pages in new tabs removed browser back buttons, making it difficult to return to chat.

I replaced the top header with a collapsible sidebar displaying persistent icons on the left that expand to reveal labels when toggled.
Chat, Tasks, Reminders/Schedule, Healthcare, Blog DEV preview, and Settings are unified in a single view, allowing users to return to chat anytime via the icon.

The Blog DEV preview was integrated into this shared frame rather than opening external tabs.
Unsent message drafts in the chat input persist when navigating between sidebar views in the same tab.

Initially, mobile headers created excessive vertical padding on iPhone screens.
By inspecting real-device screenshots and layout dimensions, I standardized all pages to viewport-fit=auto, preventing the app from duplicating system safe area padding.

Removing an unnecessary 44px top offset and initially tightening bottom padding to 8px left the footer too cramped.
I adjusted bottom spacing to 20px during normal view and reduced it to 8px while the on-screen virtual keyboard is active.

Preventing Premature Submissions from Mobile Return Keys

On mobile devices, tapping Return in the text field previously submitted messages immediately, preventing multiline input.
Because the Enter key was bound directly to message submission, the virtual keyboard’s Return key sent drafts prematurely.

I unified Enter key behavior across mobile and desktop to insert newlines.
Submissions now require tapping the arrow button or pressing Ctrl/Cmd+Enter on desktop keyboards. On mobile, the field is explicitly marked as a newline key, preventing accidental sends during Japanese IME conversion or key repeat events.

Input field clipping was not solely a padding issue.
When drafts or attachments expanded, input fields occasionally overflowed beyond the viewport during keyboard display. I grouped attachments, status indicators, location chips, and suggestion chips into a scrollable container. The text input and submit button sit outside this container, and input field height dynamically adapts to visible screen dimensions.

Following these adjustments, I verified layout behavior in desktop and mobile viewport emulators, as well as an iOS 27.2 home screen PWA via Simulator.
The mobile Return key inserts newlines without triggering submissions, the arrow button submits single entries, and line breaks persist after saving. Even with 8 image attachments and 18 lines of text, the input field and submit button remained fully visible above the virtual keyboard.

Persistence checks were performed using an isolated database, and live screen tests verified rendering and newline behavior without posting messages.
Final verification on physical iPhone hardware remains pending.