@@ -4,7 +4,10 @@ import os from "node:os";
44import path from "node:path" ;
55import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
66import type { RuntimeEnv } from "../runtime.js" ;
7- import { resolveTrajectoryPointerFilePath } from "../trajectory/paths.js" ;
7+ import {
8+ resolveTrajectoryFilePath ,
9+ resolveTrajectoryPointerFilePath ,
10+ } from "../trajectory/paths.js" ;
811import type { TrajectoryEvent } from "../trajectory/types.js" ;
912import { sessionsTailCommand , setSessionsTailFollowIntervalMsForTests } from "./sessions-tail.js" ;
1013
@@ -77,12 +80,15 @@ describe("sessionsTailCommand", () => {
7780 let storePath : string ;
7881 let trajectoryPath : string ;
7982 let previousStateDir : string | undefined ;
83+ let previousTrajectoryDir : string | undefined ;
8084
8185 beforeEach ( ( ) => {
8286 setSessionsTailFollowIntervalMsForTests ( 10 ) ;
8387 previousStateDir = process . env . OPENCLAW_STATE_DIR ;
88+ previousTrajectoryDir = process . env . OPENCLAW_TRAJECTORY_DIR ;
8489 tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "openclaw-sessions-tail-" ) ) ;
8590 process . env . OPENCLAW_STATE_DIR = path . join ( tmpDir , "state" ) ;
91+ delete process . env . OPENCLAW_TRAJECTORY_DIR ;
8692 mocks . getRuntimeConfig . mockReturnValue ( {
8793 agents : {
8894 list : [ { id : "main" } , { id : "ops" } ] ,
@@ -110,6 +116,11 @@ describe("sessionsTailCommand", () => {
110116 } else {
111117 process . env . OPENCLAW_STATE_DIR = previousStateDir ;
112118 }
119+ if ( previousTrajectoryDir === undefined ) {
120+ delete process . env . OPENCLAW_TRAJECTORY_DIR ;
121+ } else {
122+ process . env . OPENCLAW_TRAJECTORY_DIR = previousTrajectoryDir ;
123+ }
113124 fs . rmSync ( tmpDir , { recursive : true , force : true } ) ;
114125 } ) ;
115126
@@ -350,4 +361,123 @@ describe("sessionsTailCommand", () => {
350361 expect ( output ) . toContain ( "bash ok" ) ;
351362 expect ( output ) . not . toContain ( "No sessions found" ) ;
352363 } ) ;
364+
365+ it ( "tails entries pinned by sessionFile when sessionId was normalized away" , async ( ) => {
366+ const runtime = makeRuntime ( ) ;
367+ const legacySessionFile = path . join ( tmpDir , "legacy-session.jsonl" ) ;
368+ const legacyTrajectoryPath = path . join ( tmpDir , "legacy-session.trajectory.jsonl" ) ;
369+ fs . writeFileSync ( legacySessionFile , "" ) ;
370+ fs . writeFileSync (
371+ storePath ,
372+ `${ JSON . stringify ( {
373+ [ sessionKey ] : {
374+ sessionFile : legacySessionFile ,
375+ updatedAt : 2 ,
376+ status : "running" ,
377+ } ,
378+ } ) } \n`,
379+ ) ;
380+ writeJsonl ( legacyTrajectoryPath , [
381+ makeEvent ( {
382+ type : "tool.result" ,
383+ ts : "2026-05-18T12:04:21.000Z" ,
384+ data : { name : "legacy" , success : true } ,
385+ } ) ,
386+ ] ) ;
387+
388+ await sessionsTailCommand ( { store : storePath , sessionKey } , runtime ) ;
389+
390+ const output = runtimeOutput ( runtime ) ;
391+ expect ( output ) . toContain ( "tool.result" ) ;
392+ expect ( output ) . toContain ( "legacy ok" ) ;
393+ expect ( output ) . not . toContain ( "No sessions found" ) ;
394+ } ) ;
395+
396+ it ( "uses trajectory pointers for sessionFile-only entries with original runtime ids" , async ( ) => {
397+ const runtime = makeRuntime ( ) ;
398+ const legacySessionFile = path . join ( tmpDir , "legacy-session.jsonl" ) ;
399+ const pointerPath = resolveTrajectoryPointerFilePath ( legacySessionFile ) ;
400+ const runtimeTrajectoryPath = path . join ( tmpDir , "runtime-original.jsonl" ) ;
401+ fs . writeFileSync ( legacySessionFile , "" ) ;
402+ fs . writeFileSync (
403+ storePath ,
404+ `${ JSON . stringify ( {
405+ [ sessionKey ] : {
406+ sessionFile : legacySessionFile ,
407+ updatedAt : 2 ,
408+ status : "running" ,
409+ } ,
410+ } ) } \n`,
411+ ) ;
412+ fs . writeFileSync (
413+ pointerPath ,
414+ `${ JSON . stringify ( {
415+ sessionId : "runtime-original" ,
416+ runtimeFile : runtimeTrajectoryPath ,
417+ } ) } \n`,
418+ ) ;
419+ writeJsonl ( runtimeTrajectoryPath , [
420+ makeEvent ( {
421+ sessionId : "runtime-original" ,
422+ type : "tool.result" ,
423+ ts : "2026-05-18T12:04:21.000Z" ,
424+ data : { name : "pointer" , success : true } ,
425+ } ) ,
426+ ] ) ;
427+
428+ await sessionsTailCommand ( { store : storePath , sessionKey } , runtime ) ;
429+
430+ const output = runtimeOutput ( runtime ) ;
431+ expect ( output ) . toContain ( "tool.result" ) ;
432+ expect ( output ) . toContain ( "pointer ok" ) ;
433+ expect ( output ) . not . toContain ( "No sessions found" ) ;
434+ } ) ;
435+
436+ it ( "keeps trajectory directory fallback for pointer session ids" , async ( ) => {
437+ const runtime = makeRuntime ( ) ;
438+ const legacySessionFile = path . join ( tmpDir , "legacy-session.jsonl" ) ;
439+ const pointerPath = resolveTrajectoryPointerFilePath ( legacySessionFile ) ;
440+ const trajectoryDir = path . join ( tmpDir , "trajectories" ) ;
441+ process . env . OPENCLAW_TRAJECTORY_DIR = trajectoryDir ;
442+ fs . mkdirSync ( trajectoryDir , { recursive : true } ) ;
443+ fs . writeFileSync ( legacySessionFile , "" ) ;
444+ fs . writeFileSync (
445+ storePath ,
446+ `${ JSON . stringify ( {
447+ [ sessionKey ] : {
448+ sessionFile : legacySessionFile ,
449+ updatedAt : 2 ,
450+ status : "running" ,
451+ } ,
452+ } ) } \n`,
453+ ) ;
454+ fs . writeFileSync (
455+ pointerPath ,
456+ `${ JSON . stringify ( {
457+ sessionId : "runtime-original" ,
458+ runtimeFile : path . join ( tmpDir , "missing-pointer-runtime.jsonl" ) ,
459+ } ) } \n`,
460+ ) ;
461+ writeJsonl (
462+ resolveTrajectoryFilePath ( {
463+ sessionFile : legacySessionFile ,
464+ sessionId : "runtime-original" ,
465+ } ) ,
466+ [
467+ makeEvent ( {
468+ sessionId : "runtime-original" ,
469+ type : "tool.result" ,
470+ ts : "2026-05-18T12:04:21.000Z" ,
471+ data : { name : "trajectory-dir" , success : true } ,
472+ } ) ,
473+ ] ,
474+ ) ;
475+
476+ await sessionsTailCommand ( { store : storePath , sessionKey } , runtime ) ;
477+
478+ const output = runtimeOutput ( runtime ) ;
479+ expect ( output ) . toContain ( "tool.result" ) ;
480+ expect ( output ) . toContain ( "trajectory-dir ok" ) ;
481+ expect ( output ) . not . toContain ( "No sessions found" ) ;
482+ } ) ;
353483} ) ;
0 commit comments