Spaces:
Sleeping
Sleeping
| defined( 'ABSPATH' ) || exit; | |
| /** | |
| * Token Log — append-only audit of LLM invocations (Board #16). | |
| * | |
| * Each call to an LLM provider (OpenAI today; pluggable) writes one row. | |
| * The row optionally links to a downstream actor-authored comment via | |
| * (comment_kind, comment_id) once the future invocation surface ships | |
| * and starts attributing AI-driven commits to actors. | |
| * | |
| * Two consumers: | |
| * 1. Sara UI — reads usage from the live response (NOT from this table) | |
| * to render per-turn + session totals in the bubble. Session total is | |
| * JS-only; this table is server-side audit. | |
| * 2. Board #23 handoff layer — its filter callback queries | |
| * has_token_for_comment(kind, comment_id) to resolve operator mode. | |
| * Returns true if a row matches, false otherwise. Always deterministic. | |
| * | |
| * Doctrine constraints honored: | |
| * - No cost calculation. No analytics. No replay. | |
| * - Server-side persistence is for cross-session attribution (the #23 | |
| * hook). The Sara UI counters remain session-only as locked. | |
| * - Strictly additive: one new table; no existing table mutated. | |
| */ | |
| class SA_Orch_Token_Log { | |
| private static function table(): string { | |
| global $wpdb; | |
| return $wpdb->prefix . SA_ORCH_DB_PREFIX . 'token_log'; | |
| } | |
| /** | |
| * Record one LLM invocation. Returns the new row id, or 0 on failure. | |
| * | |
| * @param string $provider e.g. 'openai' | |
| * @param string $model e.g. 'gpt-4o-mini' | |
| * @param array $usage [ prompt_tokens, completion_tokens, total_tokens ] | |
| * @param int $user_id the WP user_id who triggered the call (or 0) | |
| */ | |
| public static function record( string $provider, string $model, array $usage, int $user_id = 0 ): int { | |
| if ( $provider === '' ) return 0; | |
| global $wpdb; | |
| $ok = $wpdb->insert( self::table(), [ | |
| 'user_id' => $user_id > 0 ? $user_id : null, | |
| 'provider' => $provider, | |
| 'model' => $model, | |
| 'prompt_tokens' => isset( $usage['prompt_tokens'] ) ? (int) $usage['prompt_tokens'] : 0, | |
| 'completion_tokens' => isset( $usage['completion_tokens'] ) ? (int) $usage['completion_tokens'] : 0, | |
| 'total_tokens' => isset( $usage['total_tokens'] ) ? (int) $usage['total_tokens'] : 0, | |
| 'comment_kind' => null, | |
| 'comment_id' => null, | |
| 'invoked_at' => current_time( 'mysql', true ), | |
| ] ); | |
| return $ok !== false ? (int) $wpdb->insert_id : 0; | |
| } | |
| /** | |
| * Link a previously-recorded token row to the comment it produced. | |
| * Called by the future invocation surface when an actor-attributed | |
| * comment is committed from a Sara draft. | |
| * | |
| * Idempotent on the row's identity — re-linking just updates the row. | |
| */ | |
| public static function link_to_comment( int $log_id, string $kind, int $comment_id ): bool { | |
| if ( $log_id <= 0 || $comment_id <= 0 ) return false; | |
| $kind = sanitize_key( $kind ); | |
| if ( $kind === '' ) return false; | |
| global $wpdb; | |
| $ok = $wpdb->update( | |
| self::table(), | |
| [ | |
| 'comment_kind' => $kind, | |
| 'comment_id' => $comment_id, | |
| ], | |
| [ 'id' => $log_id ] | |
| ); | |
| return $ok !== false; | |
| } | |
| /** | |
| * Filter callback for Board #23's 'sa_orch_handoff_has_llm_token'. | |
| * Returns true if at least one token row is linked to (kind, comment_id), | |
| * false otherwise. Always deterministic — no nulls. | |
| */ | |
| public static function has_token_for_comment( string $kind, int $comment_id ): bool { | |
| if ( $comment_id <= 0 ) return false; | |
| $kind = sanitize_key( $kind ); | |
| if ( $kind === '' ) return false; | |
| global $wpdb; | |
| $count = (int) $wpdb->get_var( $wpdb->prepare( | |
| "SELECT COUNT(*) FROM " . self::table() . " | |
| WHERE comment_kind = %s AND comment_id = %d", | |
| $kind, | |
| $comment_id | |
| ) ); | |
| return $count > 0; | |
| } | |
| /** | |
| * Bridge to the Board #23 hook. Hooked at plugin load via add_filter. | |
| * Replaces the default 'unknown' (null) with a deterministic boolean. | |
| * | |
| * @param mixed $default The default value (typically null). | |
| * @param string $kind Comment kind constant. | |
| * @param int $comment_id Comment id. | |
| * @return bool | |
| */ | |
| public static function answer_handoff_filter( $default, string $kind, int $comment_id ): bool { | |
| return self::has_token_for_comment( $kind, $comment_id ); | |
| } | |
| } | |