@@ -1505,6 +1505,329 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {
15051505 } ) ,
15061506 ) ;
15071507
1508+ it . effect ( "clears stale pending approvals from projected shell summaries" , ( ) =>
1509+ Effect . gen ( function * ( ) {
1510+ const projectionPipeline = yield * OrchestrationProjectionPipeline ;
1511+ const eventStore = yield * OrchestrationEventStore ;
1512+ const sql = yield * SqlClient . SqlClient ;
1513+ const appendAndProject = ( event : Parameters < typeof eventStore . append > [ 0 ] ) =>
1514+ eventStore
1515+ . append ( event )
1516+ . pipe ( Effect . flatMap ( ( savedEvent ) => projectionPipeline . projectEvent ( savedEvent ) ) ) ;
1517+
1518+ yield * appendAndProject ( {
1519+ type : "project.created" ,
1520+ eventId : EventId . make ( "evt-stale-approval-1" ) ,
1521+ aggregateKind : "project" ,
1522+ aggregateId : ProjectId . make ( "project-stale-approval" ) ,
1523+ occurredAt : "2026-02-26T12:30:00.000Z" ,
1524+ commandId : CommandId . make ( "cmd-stale-approval-1" ) ,
1525+ causationEventId : null ,
1526+ correlationId : CorrelationId . make ( "cmd-stale-approval-1" ) ,
1527+ metadata : { } ,
1528+ payload : {
1529+ projectId : ProjectId . make ( "project-stale-approval" ) ,
1530+ title : "Project Stale Approval" ,
1531+ workspaceRoot : "/tmp/project-stale-approval" ,
1532+ defaultModelSelection : null ,
1533+ scripts : [ ] ,
1534+ createdAt : "2026-02-26T12:30:00.000Z" ,
1535+ updatedAt : "2026-02-26T12:30:00.000Z" ,
1536+ } ,
1537+ } ) ;
1538+
1539+ yield * appendAndProject ( {
1540+ type : "thread.created" ,
1541+ eventId : EventId . make ( "evt-stale-approval-2" ) ,
1542+ aggregateKind : "thread" ,
1543+ aggregateId : ThreadId . make ( "thread-stale-approval" ) ,
1544+ occurredAt : "2026-02-26T12:30:01.000Z" ,
1545+ commandId : CommandId . make ( "cmd-stale-approval-2" ) ,
1546+ causationEventId : null ,
1547+ correlationId : CorrelationId . make ( "cmd-stale-approval-2" ) ,
1548+ metadata : { } ,
1549+ payload : {
1550+ threadId : ThreadId . make ( "thread-stale-approval" ) ,
1551+ projectId : ProjectId . make ( "project-stale-approval" ) ,
1552+ title : "Thread Stale Approval" ,
1553+ modelSelection : {
1554+ provider : "codex" ,
1555+ model : "gpt-5-codex" ,
1556+ } ,
1557+ runtimeMode : "approval-required" ,
1558+ interactionMode : "default" ,
1559+ branch : null ,
1560+ worktreePath : null ,
1561+ createdAt : "2026-02-26T12:30:01.000Z" ,
1562+ updatedAt : "2026-02-26T12:30:01.000Z" ,
1563+ } ,
1564+ } ) ;
1565+
1566+ yield * appendAndProject ( {
1567+ type : "thread.activity-appended" ,
1568+ eventId : EventId . make ( "evt-stale-approval-3" ) ,
1569+ aggregateKind : "thread" ,
1570+ aggregateId : ThreadId . make ( "thread-stale-approval" ) ,
1571+ occurredAt : "2026-02-26T12:30:02.000Z" ,
1572+ commandId : CommandId . make ( "cmd-stale-approval-3" ) ,
1573+ causationEventId : null ,
1574+ correlationId : CorrelationId . make ( "cmd-stale-approval-3" ) ,
1575+ metadata : { } ,
1576+ payload : {
1577+ threadId : ThreadId . make ( "thread-stale-approval" ) ,
1578+ activity : {
1579+ id : EventId . make ( "activity-stale-approval-requested" ) ,
1580+ tone : "approval" ,
1581+ kind : "approval.requested" ,
1582+ summary : "Command approval requested" ,
1583+ payload : {
1584+ requestId : "approval-request-stale-1" ,
1585+ requestKind : "command" ,
1586+ } ,
1587+ turnId : null ,
1588+ createdAt : "2026-02-26T12:30:02.000Z" ,
1589+ } ,
1590+ } ,
1591+ } ) ;
1592+
1593+ yield * appendAndProject ( {
1594+ type : "thread.activity-appended" ,
1595+ eventId : EventId . make ( "evt-stale-approval-4" ) ,
1596+ aggregateKind : "thread" ,
1597+ aggregateId : ThreadId . make ( "thread-stale-approval" ) ,
1598+ occurredAt : "2026-02-26T12:30:03.000Z" ,
1599+ commandId : CommandId . make ( "cmd-stale-approval-4" ) ,
1600+ causationEventId : null ,
1601+ correlationId : CorrelationId . make ( "cmd-stale-approval-4" ) ,
1602+ metadata : { } ,
1603+ payload : {
1604+ threadId : ThreadId . make ( "thread-stale-approval" ) ,
1605+ activity : {
1606+ id : EventId . make ( "activity-stale-approval-failed" ) ,
1607+ tone : "error" ,
1608+ kind : "provider.approval.respond.failed" ,
1609+ summary : "Provider approval response failed" ,
1610+ payload : {
1611+ requestId : "approval-request-stale-1" ,
1612+ detail : "Unknown pending permission request: approval-request-stale-1" ,
1613+ } ,
1614+ turnId : null ,
1615+ createdAt : "2026-02-26T12:30:03.000Z" ,
1616+ } ,
1617+ } ,
1618+ } ) ;
1619+
1620+ const approvalRows = yield * sql < {
1621+ readonly requestId : string ;
1622+ readonly status : string ;
1623+ readonly resolvedAt : string | null ;
1624+ } > `
1625+ SELECT
1626+ request_id AS "requestId",
1627+ status,
1628+ resolved_at AS "resolvedAt"
1629+ FROM projection_pending_approvals
1630+ WHERE request_id = 'approval-request-stale-1'
1631+ ` ;
1632+ assert . deepEqual ( approvalRows , [
1633+ {
1634+ requestId : "approval-request-stale-1" ,
1635+ status : "resolved" ,
1636+ resolvedAt : "2026-02-26T12:30:03.000Z" ,
1637+ } ,
1638+ ] ) ;
1639+
1640+ const threadRows = yield * sql < {
1641+ readonly pendingApprovalCount : number ;
1642+ } > `
1643+ SELECT pending_approval_count AS "pendingApprovalCount"
1644+ FROM projection_threads
1645+ WHERE thread_id = 'thread-stale-approval'
1646+ ` ;
1647+ assert . deepEqual ( threadRows , [ { pendingApprovalCount : 0 } ] ) ;
1648+ } ) ,
1649+ ) ;
1650+
1651+ it . effect ( "ignores non-stale provider approval response failures" , ( ) =>
1652+ Effect . gen ( function * ( ) {
1653+ const projectionPipeline = yield * OrchestrationProjectionPipeline ;
1654+ const eventStore = yield * OrchestrationEventStore ;
1655+ const sql = yield * SqlClient . SqlClient ;
1656+ const appendAndProject = ( event : Parameters < typeof eventStore . append > [ 0 ] ) =>
1657+ eventStore
1658+ . append ( event )
1659+ . pipe ( Effect . flatMap ( ( savedEvent ) => projectionPipeline . projectEvent ( savedEvent ) ) ) ;
1660+
1661+ yield * appendAndProject ( {
1662+ type : "project.created" ,
1663+ eventId : EventId . make ( "evt-nonstale-approval-1" ) ,
1664+ aggregateKind : "project" ,
1665+ aggregateId : ProjectId . make ( "project-nonstale-approval" ) ,
1666+ occurredAt : "2026-02-26T12:45:00.000Z" ,
1667+ commandId : CommandId . make ( "cmd-nonstale-approval-1" ) ,
1668+ causationEventId : null ,
1669+ correlationId : CorrelationId . make ( "cmd-nonstale-approval-1" ) ,
1670+ metadata : { } ,
1671+ payload : {
1672+ projectId : ProjectId . make ( "project-nonstale-approval" ) ,
1673+ title : "Project Non-Stale Approval" ,
1674+ workspaceRoot : "/tmp/project-nonstale-approval" ,
1675+ defaultModelSelection : null ,
1676+ scripts : [ ] ,
1677+ createdAt : "2026-02-26T12:45:00.000Z" ,
1678+ updatedAt : "2026-02-26T12:45:00.000Z" ,
1679+ } ,
1680+ } ) ;
1681+
1682+ yield * appendAndProject ( {
1683+ type : "thread.created" ,
1684+ eventId : EventId . make ( "evt-nonstale-approval-2" ) ,
1685+ aggregateKind : "thread" ,
1686+ aggregateId : ThreadId . make ( "thread-nonstale-approval" ) ,
1687+ occurredAt : "2026-02-26T12:45:01.000Z" ,
1688+ commandId : CommandId . make ( "cmd-nonstale-approval-2" ) ,
1689+ causationEventId : null ,
1690+ correlationId : CorrelationId . make ( "cmd-nonstale-approval-2" ) ,
1691+ metadata : { } ,
1692+ payload : {
1693+ threadId : ThreadId . make ( "thread-nonstale-approval" ) ,
1694+ projectId : ProjectId . make ( "project-nonstale-approval" ) ,
1695+ title : "Thread Non-Stale Approval" ,
1696+ modelSelection : {
1697+ provider : "codex" ,
1698+ model : "gpt-5-codex" ,
1699+ } ,
1700+ runtimeMode : "approval-required" ,
1701+ interactionMode : "default" ,
1702+ branch : null ,
1703+ worktreePath : null ,
1704+ createdAt : "2026-02-26T12:45:01.000Z" ,
1705+ updatedAt : "2026-02-26T12:45:01.000Z" ,
1706+ } ,
1707+ } ) ;
1708+
1709+ yield * appendAndProject ( {
1710+ type : "thread.activity-appended" ,
1711+ eventId : EventId . make ( "evt-nonstale-approval-3" ) ,
1712+ aggregateKind : "thread" ,
1713+ aggregateId : ThreadId . make ( "thread-nonstale-approval" ) ,
1714+ occurredAt : "2026-02-26T12:45:02.000Z" ,
1715+ commandId : CommandId . make ( "cmd-nonstale-approval-3" ) ,
1716+ causationEventId : null ,
1717+ correlationId : CorrelationId . make ( "cmd-nonstale-approval-3" ) ,
1718+ metadata : { } ,
1719+ payload : {
1720+ threadId : ThreadId . make ( "thread-nonstale-approval" ) ,
1721+ activity : {
1722+ id : EventId . make ( "activity-nonstale-approval-requested" ) ,
1723+ tone : "approval" ,
1724+ kind : "approval.requested" ,
1725+ summary : "Command approval requested" ,
1726+ payload : {
1727+ requestId : "approval-request-nonstale-existing" ,
1728+ requestKind : "command" ,
1729+ } ,
1730+ turnId : null ,
1731+ createdAt : "2026-02-26T12:45:02.000Z" ,
1732+ } ,
1733+ } ,
1734+ } ) ;
1735+
1736+ yield * appendAndProject ( {
1737+ type : "thread.activity-appended" ,
1738+ eventId : EventId . make ( "evt-nonstale-approval-4" ) ,
1739+ aggregateKind : "thread" ,
1740+ aggregateId : ThreadId . make ( "thread-nonstale-approval" ) ,
1741+ occurredAt : "2026-02-26T12:45:03.000Z" ,
1742+ commandId : CommandId . make ( "cmd-nonstale-approval-4" ) ,
1743+ causationEventId : null ,
1744+ correlationId : CorrelationId . make ( "cmd-nonstale-approval-4" ) ,
1745+ metadata : { } ,
1746+ payload : {
1747+ threadId : ThreadId . make ( "thread-nonstale-approval" ) ,
1748+ activity : {
1749+ id : EventId . make ( "activity-nonstale-approval-failed-existing" ) ,
1750+ tone : "error" ,
1751+ kind : "provider.approval.respond.failed" ,
1752+ summary : "Provider approval response failed" ,
1753+ payload : {
1754+ requestId : "approval-request-nonstale-existing" ,
1755+ detail : "Provider timed out while responding to approval request" ,
1756+ } ,
1757+ turnId : TurnId . make ( "turn-nonstale-failure" ) ,
1758+ createdAt : "2026-02-26T12:45:03.000Z" ,
1759+ } ,
1760+ } ,
1761+ } ) ;
1762+
1763+ yield * appendAndProject ( {
1764+ type : "thread.activity-appended" ,
1765+ eventId : EventId . make ( "evt-nonstale-approval-5" ) ,
1766+ aggregateKind : "thread" ,
1767+ aggregateId : ThreadId . make ( "thread-nonstale-approval" ) ,
1768+ occurredAt : "2026-02-26T12:45:04.000Z" ,
1769+ commandId : CommandId . make ( "cmd-nonstale-approval-5" ) ,
1770+ causationEventId : null ,
1771+ correlationId : CorrelationId . make ( "cmd-nonstale-approval-5" ) ,
1772+ metadata : { } ,
1773+ payload : {
1774+ threadId : ThreadId . make ( "thread-nonstale-approval" ) ,
1775+ activity : {
1776+ id : EventId . make ( "activity-nonstale-approval-failed-missing" ) ,
1777+ tone : "error" ,
1778+ kind : "provider.approval.respond.failed" ,
1779+ summary : "Provider approval response failed" ,
1780+ payload : {
1781+ requestId : "approval-request-nonstale-missing" ,
1782+ detail : "Provider timed out while responding to approval request" ,
1783+ } ,
1784+ turnId : null ,
1785+ createdAt : "2026-02-26T12:45:04.000Z" ,
1786+ } ,
1787+ } ,
1788+ } ) ;
1789+
1790+ const approvalRows = yield * sql < {
1791+ readonly requestId : string ;
1792+ readonly status : string ;
1793+ readonly turnId : string | null ;
1794+ readonly createdAt : string ;
1795+ readonly resolvedAt : string | null ;
1796+ } > `
1797+ SELECT
1798+ request_id AS "requestId",
1799+ status,
1800+ turn_id AS "turnId",
1801+ created_at AS "createdAt",
1802+ resolved_at AS "resolvedAt"
1803+ FROM projection_pending_approvals
1804+ WHERE request_id IN (
1805+ 'approval-request-nonstale-existing',
1806+ 'approval-request-nonstale-missing'
1807+ )
1808+ ORDER BY request_id
1809+ ` ;
1810+ assert . deepEqual ( approvalRows , [
1811+ {
1812+ requestId : "approval-request-nonstale-existing" ,
1813+ status : "pending" ,
1814+ turnId : null ,
1815+ createdAt : "2026-02-26T12:45:02.000Z" ,
1816+ resolvedAt : null ,
1817+ } ,
1818+ ] ) ;
1819+
1820+ const threadRows = yield * sql < {
1821+ readonly pendingApprovalCount : number ;
1822+ } > `
1823+ SELECT pending_approval_count AS "pendingApprovalCount"
1824+ FROM projection_threads
1825+ WHERE thread_id = 'thread-nonstale-approval'
1826+ ` ;
1827+ assert . deepEqual ( threadRows , [ { pendingApprovalCount : 1 } ] ) ;
1828+ } ) ,
1829+ ) ;
1830+
15081831 it . effect ( "does not fallback-retain messages whose turnId is removed by revert" , ( ) =>
15091832 Effect . gen ( function * ( ) {
15101833 const projectionPipeline = yield * OrchestrationProjectionPipeline ;
0 commit comments