@@ -3,6 +3,7 @@ import fs from "node:fs";
33import os from "node:os" ;
44import path from "node:path" ;
55import { afterEach , describe , expect , it , vi } from "vitest" ;
6+ import { getReplyPayloadMetadata } from "../auto-reply/reply-payload.js" ;
67import {
78 testing as replyRunTesting ,
89 createReplyOperation ,
@@ -1276,7 +1277,7 @@ describe("runCliAgent reliability", () => {
12761277 releaseAgentEnd ( ) ;
12771278 } ) ;
12781279
1279- it ( "persists approved CLI user turns before model execution " , async ( ) => {
1280+ it ( "persists approved CLI user turns and successful assistant output " , async ( ) => {
12801281 supervisorSpawnMock . mockResolvedValueOnce (
12811282 createManagedRun ( {
12821283 reason : "exit" ,
@@ -1289,7 +1290,7 @@ describe("runCliAgent reliability", () => {
12891290 noOutputTimedOut : false ,
12901291 } ) ,
12911292 ) ;
1292- const { dir, sessionFile } = createSessionFile ( ) ;
1293+ const { dir, sessionFile, storePath } = createSessionFile ( ) ;
12931294 const onUserMessagePersisted = vi . fn ( ) ;
12941295
12951296 try {
@@ -1305,6 +1306,8 @@ describe("runCliAgent reliability", () => {
13051306 sessionFile,
13061307 workspaceDir : dir ,
13071308 prompt : "runtime prompt" ,
1309+ persistAssistantTranscript : true ,
1310+ storePath,
13081311 userTurnTranscriptRecorder : createCliUserTurnRecorder ( {
13091312 text : "display prompt" ,
13101313 sessionFile,
@@ -1316,6 +1319,9 @@ describe("runCliAgent reliability", () => {
13161319 } ) ;
13171320
13181321 expect ( result . payloads ) . toEqual ( [ { text : "hello from cli" } ] ) ;
1322+ expect ( getReplyPayloadMetadata ( result . payloads ?. [ 0 ] ?? { } ) ) . toMatchObject ( {
1323+ assistantTranscriptOwned : true ,
1324+ } ) ;
13191325 expect ( onUserMessagePersisted ) . toHaveBeenCalledOnce ( ) ;
13201326 expect ( onUserMessagePersisted ) . toHaveBeenCalledWith (
13211327 expect . objectContaining ( {
@@ -1331,12 +1337,185 @@ describe("runCliAgent reliability", () => {
13311337 content : "display prompt" ,
13321338 } ) ,
13331339 ) ;
1340+ expect ( messages ) . toContainEqual (
1341+ expect . objectContaining ( {
1342+ role : "assistant" ,
1343+ content : [ { type : "text" , text : "hello from cli" } ] ,
1344+ api : "cli" ,
1345+ provider : "codex-cli" ,
1346+ model : "gpt-5.4" ,
1347+ idempotencyKey : "cli-assistant:run-persist-cli" ,
1348+ } ) ,
1349+ ) ;
13341350 expect ( JSON . stringify ( messages ) ) . not . toContain ( "runtime prompt" ) ;
13351351 } finally {
13361352 fs . rmSync ( dir , { recursive : true , force : true } ) ;
13371353 }
13381354 } ) ;
13391355
1356+ it ( "lets before_message_write block CLI assistant persistence without delivery fallback" , async ( ) => {
1357+ const hookRunner = {
1358+ hasHooks : vi . fn ( ( hookName : string ) => hookName === "before_message_write" ) ,
1359+ runBeforeMessageWrite : vi . fn ( ( ) => ( { block : true } ) ) ,
1360+ } ;
1361+ setHookRunnerForTest ( hookRunner ) ;
1362+ supervisorSpawnMock . mockResolvedValueOnce (
1363+ createManagedRun ( {
1364+ reason : "exit" ,
1365+ exitCode : 0 ,
1366+ exitSignal : null ,
1367+ durationMs : 50 ,
1368+ stdout : "secret CLI output" ,
1369+ stderr : "" ,
1370+ timedOut : false ,
1371+ noOutputTimedOut : false ,
1372+ } ) ,
1373+ ) ;
1374+ const { dir, sessionFile, storePath } = createSessionFile ( ) ;
1375+
1376+ try {
1377+ const context = buildPreparedContext ( {
1378+ sessionKey : "agent:main:main" ,
1379+ runId : "run-blocked-cli" ,
1380+ } ) ;
1381+ const result = await runPreparedCliAgent ( {
1382+ ...context ,
1383+ params : {
1384+ ...context . params ,
1385+ agentId : "main" ,
1386+ sessionFile,
1387+ workspaceDir : dir ,
1388+ persistAssistantTranscript : true ,
1389+ storePath,
1390+ } ,
1391+ } ) ;
1392+
1393+ expect ( result . payloads ) . toEqual ( [ { text : "secret CLI output" } ] ) ;
1394+ expect ( getReplyPayloadMetadata ( result . payloads ?. [ 0 ] ?? { } ) ) . toMatchObject ( {
1395+ assistantTranscriptOwned : true ,
1396+ } ) ;
1397+ expect ( readTranscriptMessages ( sessionFile ) ) . toEqual ( [ ] ) ;
1398+ expect ( hookRunner . runBeforeMessageWrite ) . toHaveBeenCalledOnce ( ) ;
1399+ expect (
1400+ callArg ( hookRunner . runBeforeMessageWrite , 0 , 1 , "before_message_write context" ) ,
1401+ ) . toEqual ( {
1402+ agentId : "main" ,
1403+ sessionKey : "agent:main:main" ,
1404+ } ) ;
1405+ } finally {
1406+ fs . rmSync ( dir , { recursive : true , force : true } ) ;
1407+ }
1408+ } ) ;
1409+
1410+ it ( "does not append late CLI output after the session key is rebound" , async ( ) => {
1411+ supervisorSpawnMock . mockResolvedValueOnce (
1412+ createManagedRun ( {
1413+ reason : "exit" ,
1414+ exitCode : 0 ,
1415+ exitSignal : null ,
1416+ durationMs : 50 ,
1417+ stdout : "late CLI output" ,
1418+ stderr : "" ,
1419+ timedOut : false ,
1420+ noOutputTimedOut : false ,
1421+ } ) ,
1422+ ) ;
1423+ const { dir, sessionFile, storePath } = createSessionFile ( ) ;
1424+ const replacementFile = path . join ( path . dirname ( sessionFile ) , "s2.jsonl" ) ;
1425+ fs . writeFileSync (
1426+ replacementFile ,
1427+ `${ JSON . stringify ( {
1428+ type : "session" ,
1429+ version : CURRENT_SESSION_VERSION ,
1430+ id : "s2" ,
1431+ timestamp : new Date ( 0 ) . toISOString ( ) ,
1432+ cwd : dir ,
1433+ } ) } \n`,
1434+ "utf-8" ,
1435+ ) ;
1436+ fs . writeFileSync (
1437+ storePath ,
1438+ JSON . stringify ( {
1439+ "agent:main:main" : {
1440+ sessionId : "s2" ,
1441+ sessionFile : replacementFile ,
1442+ updatedAt : Date . now ( ) ,
1443+ } ,
1444+ } ) ,
1445+ "utf-8" ,
1446+ ) ;
1447+
1448+ try {
1449+ const context = buildPreparedContext ( {
1450+ sessionKey : "agent:main:main" ,
1451+ runId : "run-rebound-cli" ,
1452+ } ) ;
1453+ const result = await runPreparedCliAgent ( {
1454+ ...context ,
1455+ params : {
1456+ ...context . params ,
1457+ agentId : "main" ,
1458+ sessionFile,
1459+ workspaceDir : dir ,
1460+ persistAssistantTranscript : true ,
1461+ storePath,
1462+ } ,
1463+ } ) ;
1464+
1465+ expect ( result . payloads ) . toEqual ( [ { text : "late CLI output" } ] ) ;
1466+ expect ( getReplyPayloadMetadata ( result . payloads ?. [ 0 ] ?? { } ) ) . toMatchObject ( {
1467+ assistantTranscriptOwned : true ,
1468+ } ) ;
1469+ expect ( readTranscriptMessages ( sessionFile ) ) . toEqual ( [ ] ) ;
1470+ expect ( readTranscriptMessages ( replacementFile ) ) . toEqual ( [ ] ) ;
1471+ } finally {
1472+ fs . rmSync ( dir , { recursive : true , force : true } ) ;
1473+ }
1474+ } ) ;
1475+
1476+ it ( "does not persist private room-event assistant output" , async ( ) => {
1477+ supervisorSpawnMock . mockResolvedValueOnce (
1478+ createManagedRun ( {
1479+ reason : "exit" ,
1480+ exitCode : 0 ,
1481+ exitSignal : null ,
1482+ durationMs : 50 ,
1483+ stdout : "private ambient output" ,
1484+ stderr : "" ,
1485+ timedOut : false ,
1486+ noOutputTimedOut : false ,
1487+ } ) ,
1488+ ) ;
1489+ const { dir, sessionFile, storePath } = createSessionFile ( ) ;
1490+
1491+ try {
1492+ const context = buildPreparedContext ( {
1493+ sessionKey : "agent:main:main" ,
1494+ runId : "run-private-room-event" ,
1495+ } ) ;
1496+ const result = await runPreparedCliAgent ( {
1497+ ...context ,
1498+ params : {
1499+ ...context . params ,
1500+ agentId : "main" ,
1501+ sessionFile,
1502+ workspaceDir : dir ,
1503+ persistAssistantTranscript : true ,
1504+ storePath,
1505+ currentInboundEventKind : "room_event" ,
1506+ } ,
1507+ } ) ;
1508+
1509+ expect ( result . payloads ) . toEqual ( [ { text : "private ambient output" } ] ) ;
1510+ expect ( getReplyPayloadMetadata ( result . payloads ?. [ 0 ] ?? { } ) ) . toMatchObject ( {
1511+ assistantTranscriptOwned : true ,
1512+ } ) ;
1513+ expect ( readTranscriptMessages ( sessionFile ) ) . toEqual ( [ ] ) ;
1514+ } finally {
1515+ fs . rmSync ( dir , { recursive : true , force : true } ) ;
1516+ }
1517+ } ) ;
1518+
13401519 it ( "passes cwd to approved CLI user-turn persistence" , async ( ) => {
13411520 supervisorSpawnMock . mockResolvedValueOnce (
13421521 createManagedRun ( {
0 commit comments