import * as SqlClient from "effect/unstable/sql/SqlClient";import * as Effect from "effect/Effect";export default Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; yield* sql` ALTER TABLE projection_thread_activities ADD COLUMN user_input_request_id TEXT `; yield* sql` ALTER TABLE projection_thread_activities ADD COLUMN user_input_state TEXT CHECK (user_input_state IN ('pending', 'cleared')) `; yield* sql` UPDATE projection_thread_activities SET user_input_request_id = CASE WHEN json_valid(payload_json) THEN CASE WHEN json_type(payload_json, '$.requestId') = 'text' THEN json_extract(payload_json, '$.requestId') ELSE NULL END ELSE NULL END, user_input_state = CASE WHEN kind = 'user-input.requested' THEN 'pending' WHEN kind = 'user-input.resolved' THEN 'cleared' WHEN kind = 'provider.user-input.respond.failed' AND json_valid(payload_json) AND json_type(payload_json, '$.detail') = 'text' THEN CASE WHEN lower(json_extract(payload_json, '$.detail')) LIKE '%stale pending user-input request%' OR lower(json_extract(payload_json, '$.detail')) LIKE '%unknown pending user-input request%' OR lower(json_extract(payload_json, '$.detail')) LIKE '%unknown pending user input request%' OR lower(json_extract(payload_json, '$.detail')) LIKE '%unknown pending codex user input request%' THEN 'cleared' ELSE NULL END ELSE NULL END WHERE kind IN ( 'user-input.requested', 'user-input.resolved', 'provider.user-input.respond.failed' ) `; yield* sql` UPDATE projection_thread_activities SET user_input_state = NULL WHERE user_input_request_id IS NULL `; yield* sql` UPDATE projection_thread_activities SET user_input_request_id = NULL WHERE user_input_state IS NULL `; yield* sql` CREATE INDEX IF NOT EXISTS idx_projection_thread_activities_user_input_state ON projection_thread_activities( thread_id, user_input_request_id, created_at DESC, activity_id DESC ) WHERE user_input_state IS NOT NULL `; yield* sql` DROP INDEX IF EXISTS idx_projection_thread_activities_thread_user_input_lifecycle `; yield* sql` CREATE TABLE IF NOT EXISTS projection_thread_user_input_summaries ( thread_id TEXT PRIMARY KEY, pending_count INTEGER NOT NULL CHECK (pending_count >= 0) ) `; yield* sql` WITH ranked AS ( SELECT thread_id, user_input_request_id, user_input_state, ROW_NUMBER() OVER ( PARTITION BY thread_id, user_input_request_id ORDER BY created_at DESC, activity_id DESC ) AS row_number FROM projection_thread_activities WHERE user_input_state IS NOT NULL AND user_input_request_id IS NOT NULL ) INSERT INTO projection_thread_user_input_summaries ( thread_id, pending_count ) SELECT thread_id, SUM(CASE WHEN user_input_state = 'pending' THEN 1 ELSE 0 END) FROM ranked WHERE row_number = 1 GROUP BY thread_id `; yield* sql` INSERT OR IGNORE INTO projection_thread_user_input_summaries ( thread_id, pending_count ) SELECT thread_id, 0 FROM projection_threads `; yield* sql` CREATE INDEX IF NOT EXISTS idx_projection_thread_messages_latest_user ON projection_thread_messages(thread_id, created_at DESC, message_id DESC) WHERE role = 'user' `; yield* sql` UPDATE projection_threads SET pending_user_input_count = COALESCE(( SELECT summary.pending_count FROM projection_thread_user_input_summaries AS summary WHERE summary.thread_id = projection_threads.thread_id ), 0), latest_user_message_at = ( SELECT message.created_at FROM projection_thread_messages AS message WHERE message.thread_id = projection_threads.thread_id AND message.role = 'user' ORDER BY message.created_at DESC, message.message_id DESC LIMIT 1 ) `;});
import * as SqlClient from "effect/unstable/sql/SqlClient";
import * as Effect from "effect/Effect";
export default Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;
yield* sql`
ALTER TABLE projection_thread_activities
ADD COLUMN user_input_request_id TEXT
`;
yield* sql`
ALTER TABLE projection_thread_activities
ADD COLUMN user_input_state TEXT
CHECK (user_input_state IN ('pending', 'cleared'))
`;
yield* sql`
UPDATE projection_thread_activities
SET
user_input_request_id = CASE
WHEN json_valid(payload_json) THEN CASE
WHEN json_type(payload_json, '$.requestId') = 'text'
THEN json_extract(payload_json, '$.requestId')
ELSE NULL
END
ELSE NULL
END,
user_input_state = CASE
WHEN kind = 'user-input.requested' THEN 'pending'
WHEN kind = 'user-input.resolved' THEN 'cleared'
WHEN kind = 'provider.user-input.respond.failed'
AND json_valid(payload_json)
AND json_type(payload_json, '$.detail') = 'text'
THEN CASE
WHEN lower(json_extract(payload_json, '$.detail'))
LIKE '%stale pending user-input request%'
OR lower(json_extract(payload_json, '$.detail'))
LIKE '%unknown pending user-input request%'
OR lower(json_extract(payload_json, '$.detail'))
LIKE '%unknown pending user input request%'
OR lower(json_extract(payload_json, '$.detail'))
LIKE '%unknown pending codex user input request%'
THEN 'cleared'
ELSE NULL
END
ELSE NULL
END
WHERE kind IN (
'user-input.requested',
'user-input.resolved',
'provider.user-input.respond.failed'
)
`;
yield* sql`
UPDATE projection_thread_activities
SET user_input_state = NULL
WHERE user_input_request_id IS NULL
`;
yield* sql`
UPDATE projection_thread_activities
SET user_input_request_id = NULL
WHERE user_input_state IS NULL
`;
yield* sql`
CREATE INDEX IF NOT EXISTS idx_projection_thread_activities_user_input_state
ON projection_thread_activities(
thread_id,
user_input_request_id,
created_at DESC,
activity_id DESC
)
WHERE user_input_state IS NOT NULL
`;
yield* sql`
DROP INDEX IF EXISTS idx_projection_thread_activities_thread_user_input_lifecycle
`;
yield* sql`
CREATE TABLE IF NOT EXISTS projection_thread_user_input_summaries (
thread_id TEXT PRIMARY KEY,
pending_count INTEGER NOT NULL CHECK (pending_count >= 0)
)
`;
yield* sql`
WITH ranked AS (
SELECT
thread_id,
user_input_request_id,
user_input_state,
ROW_NUMBER() OVER (
PARTITION BY thread_id, user_input_request_id
ORDER BY created_at DESC, activity_id DESC
) AS row_number
FROM projection_thread_activities
WHERE user_input_state IS NOT NULL
AND user_input_request_id IS NOT NULL
)
INSERT INTO projection_thread_user_input_summaries (
thread_id,
pending_count
)
SELECT
thread_id,
SUM(CASE WHEN user_input_state = 'pending' THEN 1 ELSE 0 END)
FROM ranked
WHERE row_number = 1
GROUP BY thread_id
`;
yield* sql`
INSERT OR IGNORE INTO projection_thread_user_input_summaries (
thread_id,
pending_count
)
SELECT thread_id, 0
FROM projection_threads
`;
yield* sql`
CREATE INDEX IF NOT EXISTS idx_projection_thread_messages_latest_user
ON projection_thread_messages(thread_id, created_at DESC, message_id DESC)
WHERE role = 'user'
`;
yield* sql`
UPDATE projection_threads
SET
pending_user_input_count = COALESCE((
SELECT summary.pending_count
FROM projection_thread_user_input_summaries AS summary
WHERE summary.thread_id = projection_threads.thread_id
), 0),
latest_user_message_at = (
SELECT message.created_at
FROM projection_thread_messages AS message
WHERE message.thread_id = projection_threads.thread_id
AND message.role = 'user'
ORDER BY message.created_at DESC, message.message_id DESC
LIMIT 1
)
`;
});