11import { finiteSecondsToTimerSafeMilliseconds } from "@openclaw/normalization-core/number-coercion" ;
22import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce" ;
33import { runCommandWithTimeout } from "../process/exec.js" ;
4+ import { isActionCriticalOutputLine } from "./action-critical-output.js" ;
45import type { CronRunDiagnostics , CronRunOutcome , CronRunStatus , CronJob } from "./types.js" ;
56
67const DEFAULT_COMMAND_TIMEOUT_MS = 10 * 60_000 ;
@@ -24,9 +25,83 @@ function trimOutput(value: string): string | undefined {
2425 return normalizeOptionalString ( value ) ;
2526}
2627
27- function buildCommandSummary ( params : { stdout : string ; stderr : string } ) : string | undefined {
28- const stdout = trimOutput ( params . stdout ) ;
29- const stderr = trimOutput ( params . stderr ) ;
28+ function formatRecoveryHint ( jobId : string ) : string {
29+ return `Recovery: openclaw cron runs --id ${ JSON . stringify ( jobId ) } ` ;
30+ }
31+
32+ function uniquePreservedLines ( params : { output ?: string ; lines ?: string [ ] } ) : string [ ] {
33+ const output = params . output ?? "" ;
34+ const seen = new Set < string > ( ) ;
35+ const lines : string [ ] = [ ] ;
36+ for ( const line of params . lines ?? [ ] ) {
37+ const normalized = line . trimEnd ( ) ;
38+ if ( ! normalized || seen . has ( normalized ) || output . includes ( normalized ) ) {
39+ continue ;
40+ }
41+ seen . add ( normalized ) ;
42+ lines . push ( normalized ) ;
43+ }
44+ return lines ;
45+ }
46+
47+ function formatCommandStreamOutput ( params : {
48+ stream : "stdout" | "stderr" ;
49+ output : string ;
50+ preservedLines ?: string [ ] ;
51+ truncatedBytes ?: number ;
52+ jobId : string ;
53+ } ) : string | undefined {
54+ const output = trimOutput ( params . output ) ;
55+ const preservedLines = params . truncatedBytes
56+ ? uniquePreservedLines ( { output, lines : params . preservedLines } )
57+ : [ ] ;
58+ if ( ! output && preservedLines . length === 0 ) {
59+ return undefined ;
60+ }
61+ if ( ! params . truncatedBytes ) {
62+ return output ;
63+ }
64+
65+ const sections : string [ ] = [ ] ;
66+ if ( preservedLines . length > 0 ) {
67+ sections . push ( `[openclaw: preserved earlier ${ params . stream } lines omitted by output cap]` ) ;
68+ sections . push ( ...preservedLines ) ;
69+ if ( output ) {
70+ sections . push ( `[openclaw: ${ params . stream } tail]` ) ;
71+ }
72+ }
73+ if ( output ) {
74+ sections . push ( output ) ;
75+ }
76+ sections . push (
77+ `[openclaw: ${ params . stream } omitted ${ params . truncatedBytes } earlier bytes; ${ formatRecoveryHint ( params . jobId ) } ]` ,
78+ ) ;
79+ return sections . join ( "\n" ) ;
80+ }
81+
82+ function buildCommandSummary ( params : {
83+ jobId : string ;
84+ stdout : string ;
85+ stderr : string ;
86+ stdoutPreservedLines ?: string [ ] ;
87+ stderrPreservedLines ?: string [ ] ;
88+ stdoutTruncatedBytes ?: number ;
89+ stderrTruncatedBytes ?: number ;
90+ } ) : string | undefined {
91+ const stdout = formatCommandStreamOutput ( {
92+ stream : "stdout" ,
93+ output : params . stdout ,
94+ preservedLines : params . stdoutPreservedLines ,
95+ truncatedBytes : params . stdoutTruncatedBytes ,
96+ jobId : params . jobId ,
97+ } ) ;
98+ const stderr = formatCommandStreamOutput ( {
99+ stream : "stderr" ,
100+ output : params . stderr ,
101+ preservedLines : params . stderrPreservedLines ,
102+ truncatedBytes : params . stderrTruncatedBytes ,
103+ jobId : params . jobId ,
104+ } ) ;
30105 if ( stdout && stderr ) {
31106 return `stdout:\n${ stdout } \n\nstderr:\n${ stderr } ` ;
32107 }
@@ -115,6 +190,7 @@ export async function runCronCommandJob(params: {
115190 ...( payload . env ? { env : payload . env } : { } ) ,
116191 ...( noOutputTimeoutMs !== undefined ? { noOutputTimeoutMs } : { } ) ,
117192 ...( payload . outputMaxBytes !== undefined ? { maxOutputBytes : payload . outputMaxBytes } : { } ) ,
193+ preserveOutputLine : ( { line } ) => isActionCriticalOutputLine ( line ) ,
118194 ...( params . abortSignal ? { signal : params . abortSignal } : { } ) ,
119195 killProcessTree : true ,
120196 } ) ;
@@ -125,7 +201,15 @@ export async function runCronCommandJob(params: {
125201 result . termination !== "no-output-timeout" &&
126202 result . termination !== "signal" ;
127203 const status : CronRunStatus = ok ? "ok" : "error" ;
128- const summary = buildCommandSummary ( { stdout : result . stdout , stderr : result . stderr } ) ;
204+ const summary = buildCommandSummary ( {
205+ jobId : params . job . id ,
206+ stdout : result . stdout ,
207+ stderr : result . stderr ,
208+ stdoutPreservedLines : result . stdoutPreservedLines ,
209+ stderrPreservedLines : result . stderrPreservedLines ,
210+ stdoutTruncatedBytes : result . stdoutTruncatedBytes ,
211+ stderrTruncatedBytes : result . stderrTruncatedBytes ,
212+ } ) ;
129213 const error = ok
130214 ? undefined
131215 : commandErrorMessage ( {
0 commit comments