diff --git a/maskinrommet/src/agent.rs b/maskinrommet/src/agent.rs index b395b9c..7275182 100644 --- a/maskinrommet/src/agent.rs +++ b/maskinrommet/src/agent.rs @@ -55,23 +55,57 @@ pub async fn handle_agent_respond( let config = load_agent_config(db, agent_node_id).await?; - // --- Sikkerhetskontroller (forblir i maskinrommet) --- + // --- Handler-modus og sikkerhetskontroller --- - // Kill switch - let is_active: bool = sqlx::query_scalar( - "SELECT is_active FROM agent_identities WHERE node_id = $1", + // Sjekk handler_mode: internal, external, disabled + let (is_active, handler_mode): (bool, String) = sqlx::query_as::<_, (bool, String)>( + "SELECT is_active, handler_mode FROM agent_identities WHERE node_id = $1", ).bind(agent_node_id).fetch_optional(db).await - .map_err(|e| format!("DB-feil: {e}"))?.unwrap_or(false); - if !is_active { - // La jobben ligge for ekstern handler (Claude Code polling) - sqlx::query("UPDATE job_queue SET status = 'pending', started_at = NULL WHERE id = $1") + .map_err(|e| format!("DB-feil: {e}"))? + .unwrap_or((false, "disabled".to_string())); + + // Disabled: dropp jobben + if !is_active || handler_mode == "disabled" { + return Ok(serde_json::json!({"status": "skipped", "reason": "agent_disabled"})); + } + + // External: la jobben ligge for ekstern handler (Claude Code / synops-agent) + if handler_mode == "external" { + sqlx::query("UPDATE job_queue SET status = 'deferred'::job_status, started_at = NULL WHERE id = $1") .bind(job.id) .execute(db) .await .map_err(|e| format!("DB-feil ved tilbakestilling: {e}"))?; - return Ok(serde_json::json!({"status": "deferred", "reason": "agent_inactive_external_handler"})); + tracing::info!( + job_id = %job.id, + "agent_respond deferred til ekstern handler (handler_mode=external)" + ); + return Ok(serde_json::json!({"status": "deferred", "reason": "external_handler"})); } + // Paused: svar brukeren at AI er utilgjengelig + if handler_mode == "paused" { + let pause_msg = "AI-assistenten er midlertidig utilgjengelig. Meldingen din er registrert."; + // Skriv svar i chatten + sqlx::query( + "WITH new_node AS ( + INSERT INTO nodes (id, node_kind, content, visibility, created_by) + VALUES (gen_random_uuid(), 'content', $1, 'hidden', $2) + RETURNING id + ) + INSERT INTO edges (id, source_id, target_id, edge_type) + SELECT gen_random_uuid(), id, $3, 'belongs_to' FROM new_node" + ) + .bind(pause_msg) + .bind(agent_node_id) + .bind(communication_id) + .execute(db).await + .map_err(|e| format!("DB-feil ved pause-svar: {e}"))?; + return Ok(serde_json::json!({"status": "paused", "reason": "agent_paused"})); + } + + // Internal: fortsett med synops-respond (under) + // Rate limiting let count: i64 = sqlx::query_scalar::<_, Option>( "SELECT COUNT(*) FROM ai_usage_log WHERE agent_node_id = $1 AND created_at > now() - interval '1 hour'", diff --git a/maskinrommet/src/jobs.rs b/maskinrommet/src/jobs.rs index 6314858..f286809 100644 --- a/maskinrommet/src/jobs.rs +++ b/maskinrommet/src/jobs.rs @@ -882,7 +882,11 @@ pub fn start_worker( match result { Ok(Ok(res)) => { - if let Err(e) = complete_job(&db2, job.id, res).await { + // Deferred-jobber eies av ekstern handler — ikke marker completed + let is_deferred = res.get("status").and_then(|s| s.as_str()) == Some("deferred"); + if is_deferred { + tracing::info!(job_id = %job.id, "Jobb deferred til ekstern handler"); + } else if let Err(e) = complete_job(&db2, job.id, res).await { tracing::error!(job_id = %job.id, error = %e, "Kunne ikke markere jobb som fullført"); } else { tracing::info!(job_id = %job.id, "Jobb fullført"); diff --git a/scripts/vaktmester-complete.sh b/scripts/vaktmester-complete.sh new file mode 100755 index 0000000..814d77b --- /dev/null +++ b/scripts/vaktmester-complete.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Marker en deferred jobb som completed etter at Claude Code har svart. +# Bruk: ./scripts/vaktmester-complete.sh + +set -euo pipefail +JOB_ID="${1:?Mangler job-id}" + +docker exec sidelinja-postgres-1 psql -U sidelinja -d synops -q -c " +UPDATE job_queue SET status = 'completed', completed_at = now(), + result = '{\"status\":\"completed\",\"handler\":\"claude-code\"}'::jsonb +WHERE id = '$JOB_ID'; +" +echo "Jobb $JOB_ID markert som completed" diff --git a/scripts/vaktmester-poll.sh b/scripts/vaktmester-poll.sh new file mode 100755 index 0000000..6a4d8e9 --- /dev/null +++ b/scripts/vaktmester-poll.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Poll for deferred agent_respond-jobber og returner info for Claude Code. +# Brukes av Claude Code sin cron-loop. +# +# Output: JSON med jobb-info hvis det finnes en jobb, ellers "none" + +set -euo pipefail + +CHAT_ID="abe2edfd-986b-45ba-8c2e-4461a8a7e480" + +# Hent eldste deferred jobb +JOB=$(docker exec sidelinja-postgres-1 psql -U sidelinja -d synops -t -A -c " +SELECT row_to_json(t) FROM ( + SELECT j.id as job_id, j.payload, j.created_at, + n.content as message, + n.created_by as sender_id, + (SELECT title FROM nodes WHERE id = n.created_by) as sender_name + FROM job_queue j + JOIN nodes n ON n.id = (j.payload->>'message_id')::uuid + WHERE j.job_type = 'agent_respond' + AND j.status = 'deferred' + ORDER BY j.created_at ASC + LIMIT 1 +) t; +" 2>/dev/null) + +if [ -z "$JOB" ] || [ "$JOB" = "" ]; then + echo "none" + exit 0 +fi + +echo "$JOB"