1515
1616import { createInterface } from "node:readline" ;
1717import { randomUUID , randomBytes } from "node:crypto" ;
18- import { createReadStream } from "node:fs" ;
18+ import { createReadStream , readFileSync , writeFileSync } from "node:fs" ;
1919import fs from "node:fs/promises" ;
2020import path from "node:path" ;
2121
@@ -132,21 +132,18 @@ async function ensureSessionDir() {
132132}
133133
134134/** Read only the first line of a file without loading the entire content. */
135- async function readFirstLine ( filePath ) {
136- return new Promise ( ( resolve , reject ) => {
137- const stream = createReadStream ( filePath , { encoding : "utf-8" } ) ;
138- const rl = createInterface ( { input : stream , crlfDelay : Infinity } ) ;
139- rl . once ( "line" , ( line ) => {
140- rl . close ( ) ;
141- stream . destroy ( ) ;
142- resolve ( line ) ;
143- } ) ;
144- rl . once ( "close" , ( ) => resolve ( null ) ) ;
145- stream . once ( "error" , reject ) ;
146- } ) ;
135+ function readFirstLine ( filePath ) {
136+ try {
137+ const buf = readFileSync ( filePath , { encoding : "utf-8" } ) ;
138+ const nl = buf . indexOf ( "\n" ) ;
139+ const cr = buf . indexOf ( "\r" ) ;
140+ const end2 = cr >= 0 && ( nl < 0 || cr < nl ) ? cr : nl >= 0 ? nl : buf . length ;
141+ return buf . slice ( 0 , end2 ) || null ;
142+ } catch { return null ; }
147143}
148144
149- async function getOrCreateSession ( ) {
145+
146+ async function getOrCreateSession ( requestedSessionId ) {
150147 await ensureSessionDir ( ) ;
151148
152149 // Find session files and sort by mtime (most recent first)
@@ -169,6 +166,7 @@ async function getOrCreateSession() {
169166 if ( ! firstLine ) continue ;
170167 const header = JSON . parse ( firstLine ) ;
171168 if ( header . type === "session" && header . id ) {
169+ if ( requestedSessionId && header . id !== requestedSessionId ) continue ;
172170 return { sessionId : header . id , sessionFile } ;
173171 }
174172 } catch {
@@ -180,7 +178,7 @@ async function getOrCreateSession() {
180178 }
181179
182180 // Create a new session
183- const sessionId = randomUUID ( ) ;
181+ const sessionId = requestedSessionId || randomUUID ( ) ;
184182 const sessionFile = path . join ( SESSION_DIR , `session-${ sessionId } .jsonl` ) ;
185183 const header = {
186184 type : "session" ,
@@ -352,9 +350,39 @@ function abortActiveRun() {
352350// Agent execution
353351// ---------------------------------------------------------------------------
354352
353+
354+ // ── Conversation history (independent of SessionManager) ──────────
355+ function getHistoryPath ( sessionFile ) {
356+ return sessionFile . replace ( / \. j s o n l $ / , '.history.json' ) ;
357+ }
358+ function loadHistory ( sessionFile , maxTurns ) {
359+ maxTurns = maxTurns || 20 ;
360+ try {
361+ const raw = readFileSync ( getHistoryPath ( sessionFile ) , "utf-8" ) ;
362+ const arr = JSON . parse ( raw ) ;
363+ return Array . isArray ( arr ) ? arr . slice ( - maxTurns ) : [ ] ;
364+ } catch { return [ ] ; }
365+ }
366+ function saveHistory ( sessionFile , turns ) {
367+ try {
368+ writeFileSync ( getHistoryPath ( sessionFile ) , JSON . stringify ( turns ) , "utf-8" ) ;
369+ process . stderr . write ( "[openclaw-agent] history saved: " + turns . length + " turns\n" ) ;
370+ } catch ( e ) {
371+ process . stderr . write ( "[openclaw-agent] saveHistory failed: " + e . message + "\n" ) ;
372+ }
373+ }
374+ function buildHistoryPrompt ( turns ) {
375+ if ( ! turns || turns . length === 0 ) return "" ;
376+ return "<conversation_history>\n" +
377+ turns . map ( t => "[" + t . role + "]: " + t . text ) . join ( "\n" ) +
378+ "\n</conversation_history>\n\nContinuing the above conversation. User says:\n" ;
379+ }
380+
381+ let requestedSessionId = null ;
382+
355383async function runAgent ( prompt , images ) {
356384 if ( ! currentSessionId ) {
357- const session = await getOrCreateSession ( ) ;
385+ const session = await getOrCreateSession ( requestedSessionId ) ;
358386 currentSessionId = session . sessionId ;
359387 currentSessionFile = session . sessionFile ;
360388 }
@@ -419,11 +447,16 @@ async function runAgent(prompt, images) {
419447 } ;
420448 }
421449
450+ // Inject conversation history into prompt
451+ const prevTurns = loadHistory ( currentSessionFile , 20 ) ;
452+ const historyPrefix = buildHistoryPrompt ( prevTurns ) ;
453+ const effectivePrompt = historyPrefix ? historyPrefix + prompt : prompt ;
454+
422455 const result = await runEmbeddedPiAgent ( {
423456 sessionId : currentSessionId ,
424457 sessionFile : currentSessionFile ,
425458 workspaceDir : WORKSPACE_DIR ,
426- prompt,
459+ prompt : effectivePrompt ,
427460 images : images || undefined ,
428461 provider : PROVIDER ,
429462 model : MODEL ,
@@ -527,6 +560,14 @@ async function runAgent(prompt, images) {
527560 emitTextDelta ( metaText ) ;
528561 }
529562
563+ // Save conversation turn to separate history file
564+ {
565+ const hist = loadHistory ( currentSessionFile , 50 ) ;
566+ hist . push ( { role : "user" , text : prompt } ) ;
567+ if ( lastPartialText ) hist . push ( { role : "assistant" , text : lastPartialText . trim ( ) } ) ;
568+ saveHistory ( currentSessionFile , hist ) ;
569+ }
570+
530571 emitMessageEnd ( ) ;
531572
532573 // Check for errors
@@ -587,6 +628,7 @@ async function main() {
587628 MODEL = msg . model ;
588629 }
589630 if ( msg . provider ) PROVIDER = msg . provider ;
631+ if ( msg . sessionId ) requestedSessionId = msg . sessionId ;
590632 process . stderr . write ( "[openclaw-agent] config received via stdin\n" ) ;
591633 continue ;
592634 }
0 commit comments