diff --git a/daemon/reconcile.ts b/daemon/reconcile.ts index 88c1003..e39371a 100644 --- a/daemon/reconcile.ts +++ b/daemon/reconcile.ts @@ -1,13 +1,24 @@ // Transcript reconcile loop: the transcript JSONL is ground truth import * as fs from "fs"; -import type { TranscriptEntry } from "./types.ts"; import { getSession, dequeue, getSessionsForReconcile, upsertSession } from "./db.ts"; -export interface TranscriptEntry { +// Real Claude Code transcript entry shape +interface TranscriptEntry { type: string; - role?: string; - content?: string; - timestamp?: number; + message?: { + role?: string; + content?: unknown; + }; + timestamp?: string | number; // ISO-8601 string or epoch-ms number + // Non-message types: attachment, queue-operation, last-prompt, etc. +} + +// Parse timestamp from ISO string or pass through number +function parseTimestamp(ts: string | number | undefined): number { + if (ts === undefined) return 0; + if (typeof ts === "number") return ts; + const parsed = Date.parse(ts); + return isNaN(parsed) ? 0 : parsed; } // Check if a transcript has advanced past the last stuck point @@ -29,18 +40,20 @@ export function hasTranscriptAdvanced( for (let i = lines.length - checkCount; i < lines.length; i++) { try { const entry: TranscriptEntry = JSON.parse(lines[i]); - const entryTime = entry.timestamp ?? 0; + const entryTime = parseTimestamp(entry.timestamp); - // Only consider entries after the stuck time + // Only consider entries strictly newer than the stuck time if (entryTime <= lastStuckAt) continue; // User message means they answered directly in the pane - if (entry.type === "user_message" || entry.role === "user") { + // Real format: type="user" with message.role="user" + if (entry.type === "user" || entry.message?.role === "user") { return true; } // New assistant turn means the session progressed - if (entry.type === "assistant" || entry.role === "assistant") { + // Real format: type="assistant" with message.role="assistant" + if (entry.type === "assistant" || entry.message?.role === "assistant") { return true; } } catch { diff --git a/test-walking-skeleton.sh b/test-walking-skeleton.sh index 8c9e485..e17cfcc 100755 --- a/test-walking-skeleton.sh +++ b/test-walking-skeleton.sh @@ -57,11 +57,13 @@ create_session() { echo "$pane_id" } -# Helper: create a transcript file +# Helper: create a transcript file with real Claude Code format create_transcript() { local session_id=$1 local transcript_path="$TB_DIR/test-transcript-${session_id}.jsonl" - echo '{"type":"user","content":"test"}' > "$transcript_path" + # Real format: type="user" with nested message.role and ISO timestamp + local current_iso=$(date -Iseconds) + echo "{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"test\"},\"timestamp\":\"$current_iso\"}" > "$transcript_path" echo "$transcript_path" } @@ -240,9 +242,9 @@ else fi # Simulate user answering directly in pane by advancing transcript -# Timestamp must exceed last_stuck_at (epoch ms); role must be "user" -FUTURE_TS=$(( $(date +%s%3N) + 60000 )) -echo "{\"type\":\"user\",\"role\":\"user\",\"content\":\"answered directly\",\"timestamp\":$FUTURE_TS}" >> "$TRANSIENT3" +# Use real Claude Code format: type="user" with nested message.role and ISO timestamp +FUTURE_ISO=$(date -d "+60 seconds" -Iseconds) +echo "{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"answered directly\"},\"timestamp\":\"$FUTURE_ISO\"}" >> "$TRANSIENT3" # Wait for reconcile loop (5s interval, but we can trigger manually by waiting) sleep 6 @@ -295,8 +297,9 @@ fi echo "[ok] Queue state survived daemon restart (SQLite persistence)" # Advance transcript — reconcile should dequeue -FUTURE_TS=$(( $(date +%s%3N) + 60000 )) -echo "{\"type\":\"user\",\"role\":\"user\",\"content\":\"answered\",\"timestamp\":$FUTURE_TS}" >> "$TRANSIENT4" +# Use real Claude Code format with ISO timestamp +FUTURE_ISO=$(date -d "+60 seconds" -Iseconds) +echo "{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"answered\"},\"timestamp\":\"$FUTURE_ISO\"}" >> "$TRANSIENT4" sleep 6 COUNT=$(queue_count) if [ "$COUNT" -eq 0 ]; then @@ -419,6 +422,61 @@ else fi echo "[pass] AS-7 complete" +reset_daemon +# ======================================================================== +# AS-8: Real transcript format (ISO timestamps, nested message.role) +# ======================================================================== +echo "" +echo "=== AS-8: Real transcript format validation ===" + +PANE8=$(create_session "${TEST_BASE}-as8") +# Create a transcript with pre-stuck content +TRANSIENT8="$TB_DIR/test-transcript-as8.jsonl" +# Use current time minus 30 seconds for pre-stuck content +PRE_STUCK_TS=$(date -d "30 seconds ago" -Iseconds) +# Create initial user message +cat > "$TRANSIENT8" <> "$TRANSIENT8" + +# Wait for reconcile loop (5s interval) - should dequeue within one sweep +sleep 6 +COUNT=$(queue_count) +if [ "$COUNT" -eq 0 ]; then + echo "[ok] Reconcile dequeued after appending real-format user entry" +else + echo "[fail] Expected count=0 after reconcile, got $COUNT" + exit 1 +fi + +echo "[pass] AS-8 complete" + # ======================================================================== # Summary # ========================================================================