@@ -1486,6 +1486,151 @@ func TestParseClaudeSessionFrom_SameMessageIDRunMergesIncrementally(
14861486 assert .Equal (t , RoleUser , newMsgs [1 ].Role )
14871487}
14881488
1489+ // A queued_command attachment can be written mid-stream, between
1490+ // assistant chunks of one same-message.id response (observed in real
1491+ // CLI transcripts). When the sync boundary falls inside that run, the
1492+ // append window is [queued command, chunk, chunk] and the queued
1493+ // command's earlier timestamp sorts it to position 0 of the returned
1494+ // messages. The engine's cross-sync split detection compares only the
1495+ // FIRST appended message's ClaudeMessageID against the stored tail, so
1496+ // a masked head would append the continuation as a duplicate message.
1497+ // The parser must fall back to a full parse instead.
1498+ func TestParseClaudeSessionFrom_QueuedCommandBeforeContinuationFallsBack (
1499+ t * testing.T ,
1500+ ) {
1501+ t .Parallel ()
1502+
1503+ chunk := func (uuid , parent , ts , text string ) string {
1504+ return `{"type":"assistant","uuid":"` + uuid +
1505+ `","parentUuid":"` + parent +
1506+ `","timestamp":"` + ts +
1507+ `","message":{"id":"msg_split","content":[` +
1508+ `{"type":"text","text":"` + text + `"}]}}`
1509+ }
1510+
1511+ initial := testjsonl .JoinJSONL (
1512+ testjsonl .ClaudeUserJSON ("hello" , tsEarly ),
1513+ chunk ("a1" , "u1" , "2024-01-01T10:00:01Z" , "Hel" ),
1514+ )
1515+
1516+ parseFrom := func (
1517+ t * testing.T , appended string ,
1518+ ) ([]ParsedMessage , error ) {
1519+ t .Helper ()
1520+ path := createTestFile (
1521+ t , "inc-queued-boundary.jsonl" , initial ,
1522+ )
1523+ info , err := os .Stat (path )
1524+ require .NoError (t , err )
1525+ offset := info .Size ()
1526+
1527+ f , err := os .OpenFile (path , os .O_APPEND | os .O_WRONLY , 0o644 )
1528+ require .NoError (t , err )
1529+ _ , err = f .WriteString (appended )
1530+ require .NoError (t , err )
1531+ require .NoError (t , f .Close ())
1532+
1533+ msgs , _ , _ , _ , perr := claudeParseSessionFrom (
1534+ path , offset , claudeIncrementalScan {
1535+ startOrdinal : 2 ,
1536+ lastEntryUUID : "a1" ,
1537+ storedLinearParse : new (true ),
1538+ },
1539+ )
1540+ return msgs , perr
1541+ }
1542+
1543+ t .Run ("queued command masks continuation head" , func (t * testing.T ) {
1544+ t .Parallel ()
1545+
1546+ appended := testjsonl .ClaudeQueuedCommandJSON (
1547+ "queued mid-stream" , "2024-01-01T10:00:02Z" ,
1548+ ) + "\n " +
1549+ chunk ("a2" , "a1" , "2024-01-01T10:00:03Z" , "Hello wo" ) + "\n " +
1550+ chunk ("a3" , "a2" , "2024-01-01T10:00:04Z" , "Hello world" ) + "\n "
1551+
1552+ _ , perr := parseFrom (t , appended )
1553+ assert .ErrorIs (t , perr , ErrClaudeIncrementalNeedsFullParse ,
1554+ "a queued command sorting ahead of a same-message.id " +
1555+ "continuation must force a full parse" )
1556+ })
1557+
1558+ t .Run ("full parse of the whole file keeps one merged message" ,
1559+ func (t * testing.T ) {
1560+ t .Parallel ()
1561+
1562+ content := initial + "\n " + testjsonl .JoinJSONL (
1563+ testjsonl .ClaudeQueuedCommandJSON (
1564+ "queued mid-stream" , "2024-01-01T10:00:02Z" ,
1565+ ),
1566+ chunk ("a2" , "a1" , "2024-01-01T10:00:03Z" , "Hello wo" ),
1567+ chunk ("a3" , "a2" , "2024-01-01T10:00:04Z" , "Hello world" ),
1568+ )
1569+ _ , msgs := runClaudeParserTest (
1570+ t , "queued-boundary-full.jsonl" , content ,
1571+ )
1572+ require .Len (t , msgs , 3 )
1573+ assert .Equal (t , RoleUser , msgs [0 ].Role )
1574+ assert .Equal (t , "queued_command" , msgs [1 ].SourceSubtype )
1575+ assert .Equal (t , RoleAssistant , msgs [2 ].Role )
1576+ assert .Equal (t , "Hello world" , msgs [2 ].Content ,
1577+ "the run must collapse to one merged assistant message" )
1578+ ids := 0
1579+ for _ , m := range msgs {
1580+ if m .ClaudeMessageID == "msg_split" {
1581+ ids ++
1582+ }
1583+ }
1584+ assert .Equal (t , 1 , ids ,
1585+ "msg_split must appear exactly once after a full parse" )
1586+ })
1587+
1588+ t .Run ("queued command after the run stays incremental" ,
1589+ func (t * testing.T ) {
1590+ t .Parallel ()
1591+
1592+ appended := chunk (
1593+ "a2" , "a1" , "2024-01-01T10:00:03Z" , "Hello wo" ,
1594+ ) + "\n " +
1595+ chunk ("a3" , "a2" , "2024-01-01T10:00:04Z" , "Hello world" ) +
1596+ "\n " + testjsonl .ClaudeQueuedCommandJSON (
1597+ "queued after" , "2024-01-01T10:02:00Z" ,
1598+ ) + "\n "
1599+
1600+ msgs , perr := parseFrom (t , appended )
1601+ require .NoError (t , perr )
1602+ require .Len (t , msgs , 2 )
1603+ assert .Equal (t , RoleAssistant , msgs [0 ].Role )
1604+ assert .Equal (t , "msg_split" , msgs [0 ].ClaudeMessageID ,
1605+ "the continuation head must stay first so the engine's " +
1606+ "split check can see it" )
1607+ assert .Equal (t , "queued_command" , msgs [1 ].SourceSubtype )
1608+ })
1609+
1610+ t .Run ("queued command before a user head stays incremental" ,
1611+ func (t * testing.T ) {
1612+ t .Parallel ()
1613+
1614+ appended := testjsonl .ClaudeQueuedCommandJSON (
1615+ "queued early" , "2024-01-01T10:00:02Z" ,
1616+ ) + "\n " +
1617+ `{"type":"user","uuid":"u2","parentUuid":"a1",` +
1618+ `"timestamp":"2024-01-01T10:00:03Z",` +
1619+ `"message":{"content":"next turn"}}` + "\n " +
1620+ `{"type":"assistant","uuid":"a2","parentUuid":"u2",` +
1621+ `"timestamp":"2024-01-01T10:00:04Z",` +
1622+ `"message":{"id":"msg_next","content":[` +
1623+ `{"type":"text","text":"fresh"}]}}` + "\n "
1624+
1625+ msgs , perr := parseFrom (t , appended )
1626+ require .NoError (t , perr )
1627+ require .Len (t , msgs , 3 )
1628+ assert .Equal (t , "queued_command" , msgs [0 ].SourceSubtype )
1629+ assert .Equal (t , "next turn" , msgs [1 ].Content )
1630+ assert .Equal (t , "msg_next" , msgs [2 ].ClaudeMessageID )
1631+ })
1632+ }
1633+
14891634// An appended parentless entry adds a DAG root — the only property of
14901635// the stored linearity verdict an append can move toward
14911636// resolvability — so even a linear-bound session must fall back to a
0 commit comments