view mrjunejune/conversation_store.h @ 267:a78f5986011f

discover bundled production runtimes Locate Bazel-packaged Copilot and Python binaries by stable suffix instead of hardcoding an external repository prefix. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Fri, 07 Aug 2026 12:58:55 -0700
parents 04fee26ecce0
children
line wrap: on
line source

#ifndef MRJUNEJUNE_CONVERSATION_STORE_H
#define MRJUNEJUNE_CONVERSATION_STORE_H

#include "dowa/dowa.h"

typedef struct Conversation_Store Conversation_Store;

typedef enum {
  CONVERSATION_STORE_ERROR = -1,
  CONVERSATION_STORE_OK = 0,
  CONVERSATION_STORE_NOT_FOUND = 1,
  CONVERSATION_STORE_CONFLICT = 2,
} Conversation_Store_Result;

/* ------------------------------------------------------------------ */
/* Ownership types                                                      */
/* ------------------------------------------------------------------ */

typedef enum {
  CONVERSATION_OWNER_KIND_USER   = 0,
  CONVERSATION_OWNER_KIND_GUEST  = 1,
  CONVERSATION_OWNER_KIND_LEGACY = 2,
} Conversation_Owner_Kind;

/*
 * Owner identity for a conversation.
 * kind=USER/GUEST: id holds user_id or guest_id (UUID, 36 chars).
 * kind=LEGACY:     id is empty; owned by no one.
 */
typedef struct {
  Conversation_Owner_Kind kind;
  char id[37]; /* user_id or guest_id; empty string for legacy */
} Conversation_Owner;

/*
 * Lightweight summary returned by the listing endpoint.
 * All string pointers are arena-allocated.
 * last_message_preview is truncated to 200 chars (UTF-8 may split mid-char;
 * callers must not rely on it being valid UTF-8 at the boundary).
 */
typedef struct {
  char  *id;
  char  *title;
  char  *status;
  int64  created_at;
  int64  updated_at;
  int64  turn_count;
  char  *last_message_preview; /* at most 200 bytes + NUL */
} Conversation_Summary;

/* ------------------------------------------------------------------ */
/* Turn and record types                                                */
/* ------------------------------------------------------------------ */

typedef struct {
  int64 id;
  int64 sequence;
  char *role;
  char *content;
  char *status;
  char *request_id;
  char *error_message;
  int64 input_tokens;
  int64 output_tokens;
  int64 created_at;
  int64 completed_at;
} Conversation_Turn;

typedef struct {
  char *id;
  char *copilot_session_id;
  char *title;
  char *status;
  int64 created_at;
  int64 updated_at;
  Conversation_Turn *turns;
} Conversation_Record;

/* ------------------------------------------------------------------ */
/* Lifecycle                                                            */
/* ------------------------------------------------------------------ */

Conversation_Store *Conversation_Store_Create(const char *database_path);
void Conversation_Store_Destroy(Conversation_Store *p_store);
boolean Conversation_Store_Generate_UUID(char output[37]);

/* ------------------------------------------------------------------ */
/* Owner-aware APIs (production HTTP handlers must use these)           */
/* ------------------------------------------------------------------ */

/*
 * Create a conversation assigned to p_owner.
 * p_owner->kind must be USER or GUEST; LEGACY is rejected.
 */
Conversation_Store_Result Conversation_Store_Create_Owned(
    Conversation_Store *p_store,
    const char *title,
    const Conversation_Owner *p_owner,
    char output_id[37]);

/*
 * Get a conversation; returns NOT_FOUND if the conversation does not belong
 * to p_owner (existence leaks prevented).
 */
Conversation_Store_Result Conversation_Store_Get_Owned(
    Conversation_Store *p_store,
    const char *conversation_id,
    const Conversation_Owner *p_owner,
    Conversation_Record *p_record,
    Dowa_Arena *p_arena);

Conversation_Store_Result Conversation_Store_Update_Title_Owned(
    Conversation_Store *p_store,
    const char *conversation_id,
    const Conversation_Owner *p_owner,
    const char *title);

Conversation_Store_Result Conversation_Store_Delete_Owned(
    Conversation_Store *p_store,
    const char *conversation_id,
    const Conversation_Owner *p_owner);

Conversation_Store_Result Conversation_Store_Begin_Turn_Owned(
    Conversation_Store *p_store,
    const char *conversation_id,
    const Conversation_Owner *p_owner,
    const char *request_id,
    const char *prompt);

/*
 * List conversations for an owner, ordered newest-first.
 * Legacy conversations are never listed (returns empty list for LEGACY owner).
 *
 * cursor_updated_at / cursor_id: pass 0 / NULL for the first page.
 * limit: capped to 50 internally; must be >= 1.
 *
 * *pp_summaries is arena-allocated; *p_count is the number of items.
 * A cursor for the next page (if *p_count == limit) can be derived from
 * (*pp_summaries)[*p_count - 1].{updated_at, id}.
 */
Conversation_Store_Result Conversation_Store_List(
    Conversation_Store *p_store,
    const Conversation_Owner *p_owner,
    int64 cursor_updated_at,
    const char *cursor_id,
    int32 limit,
    Conversation_Summary **pp_summaries,
    int32 *p_count,
    Dowa_Arena *p_arena);

/*
 * Claim a legacy conversation: reassign it to user_id.
 * Returns NOT_FOUND if the conversation is not a legacy row (or does not
 * exist), preventing existence leaks.
 * Returns CONFLICT if the conversation is already owned by someone else.
 */
Conversation_Store_Result Conversation_Store_Claim_Legacy(
    Conversation_Store *p_store,
    const char *conversation_id,
    const char *user_id);

/*
 * Atomically transfer all guest conversations to a user.
 * Idempotent: re-running with the same (guest_id, user_id) pair is safe.
 * Returns CONVERSATION_STORE_OK on success (including the no-op case).
 */
Conversation_Store_Result Conversation_Store_Transfer_Guest_To_User(
    Conversation_Store *p_store,
    const char *guest_id,
    const char *user_id);

/*
 * Like Conversation_Store_Transfer_Guest_To_User but also clears outstanding
 * guest quota reservations atomically in the same transaction.
 * Use this on login: both the conversation transfer and quota cleanup commit
 * or roll back together.  The quota tables must reside in the same SQLite
 * database file as the conversation tables (the shared mrjunejune.db).
 */
Conversation_Store_Result Conversation_Store_Transfer_Guest_To_User_Atomic(
    Conversation_Store *p_store,
    const char *guest_id,
    const char *user_id);

/* ------------------------------------------------------------------ */
/* Legacy/internal APIs (kept only for migration-compatibility tests)   */
/* Production HTTP handlers must not use these.                         */
/* ------------------------------------------------------------------ */

Conversation_Store_Result Conversation_Store_Create_Conversation(
    Conversation_Store *p_store,
    const char *title,
    char output_id[37]);

Conversation_Store_Result Conversation_Store_Get(
    Conversation_Store *p_store,
    const char *conversation_id,
    Conversation_Record *p_record,
    Dowa_Arena *p_arena);

Conversation_Store_Result Conversation_Store_Update_Title(
    Conversation_Store *p_store,
    const char *conversation_id,
    const char *title);

Conversation_Store_Result Conversation_Store_Delete(
    Conversation_Store *p_store,
    const char *conversation_id);

Conversation_Store_Result Conversation_Store_Begin_Turn(
    Conversation_Store *p_store,
    const char *conversation_id,
    const char *request_id,
    const char *prompt);

Conversation_Store_Result Conversation_Store_Complete_Turn(
    Conversation_Store *p_store,
    const char *conversation_id,
    const char *request_id,
    const char *content,
    int64 input_tokens,
    int64 output_tokens);

Conversation_Store_Result Conversation_Store_Fail_Turn(
    Conversation_Store *p_store,
    const char *conversation_id,
    const char *request_id,
    const char *error_message,
    boolean aborted);

#endif