@@ -87,6 +87,7 @@ export interface FailAgentCallInput {
8787
8888export interface CompleteRunInput {
8989 resultJson ?: string ;
90+ callCount ?: number ;
9091}
9192
9293export interface FailRunInput {
@@ -97,6 +98,7 @@ export interface FailRunInput {
9798export interface DrainEventsResult {
9899 events : WorkflowEventRecord [ ] ;
99100 nextSeq : number ;
101+ hasMore : boolean ;
100102 terminal : boolean ;
101103 run : WorkflowRunRecord ;
102104}
@@ -409,11 +411,14 @@ export class WorkflowStore {
409411 const updated = Result . try ( {
410412 try : ( ) => {
411413 const now = isoNow ( ) ;
412- this . database . sqlite
414+ const update = this . database . sqlite
413415 . prepare (
414- `update workflow_runs set cancel_requested = 'true', updated_at = ? where id = ?` ,
416+ `update workflow_runs
417+ set cancel_requested = 'true', updated_at = ?
418+ where id = ? and status in ('starting', 'running')` ,
415419 )
416420 . run ( now , id ) ;
421+ if ( update . changes === 0 ) return this . getRun ( id ) ;
417422 return this . getRun ( id ) ;
418423 } ,
419424 catch : ( cause ) => new WorkflowStoreError ( "request_cancel" , cause ) ,
@@ -439,18 +444,31 @@ export class WorkflowStore {
439444 return this . transitionRunResult ( id , "complete" , ( ) => {
440445 if ( input . resultJson !== undefined ) assertResultSize ( input . resultJson ) ;
441446 const now = isoNow ( ) ;
442- return this . database . sqlite
443- . prepare (
444- `update workflow_runs set
445- status = 'completed',
446- result_json = ?,
447- completed_at = ?,
448- updated_at = ?,
449- error = null,
450- error_kind = null
451- where id = ? and status in ('starting', 'running')` ,
452- )
453- . run ( input . resultJson ?? null , now , now , id ) . changes ;
447+ const transaction = this . database . sqlite . transaction ( ( ) => {
448+ const changes = this . database . sqlite
449+ . prepare (
450+ `update workflow_runs set
451+ status = 'completed',
452+ result_json = ?,
453+ completed_at = ?,
454+ updated_at = ?,
455+ error = null,
456+ error_kind = null
457+ where id = ? and status in ('starting', 'running')` ,
458+ )
459+ . run ( input . resultJson ?? null , now , now , id ) . changes ;
460+ if ( changes === 0 ) return 0 ;
461+ this . insertEventRow (
462+ {
463+ runId : id ,
464+ type : "run_completed" ,
465+ data : { callCount : input . callCount ?? 0 } ,
466+ } ,
467+ now ,
468+ ) ;
469+ return changes ;
470+ } ) ;
471+ return transaction . immediate ( ) ;
454472 } ) ;
455473 }
456474
@@ -464,17 +482,31 @@ export class WorkflowStore {
464482 ) : BetterResult < WorkflowRunRecord , WorkflowRunTransitionError > {
465483 return this . transitionRunResult ( id , "fail" , ( ) => {
466484 const now = isoNow ( ) ;
467- return this . database . sqlite
468- . prepare (
469- `update workflow_runs set
470- status = 'failed',
471- error = ?,
472- error_kind = ?,
473- completed_at = ?,
474- updated_at = ?
475- where id = ? and status in ('starting', 'running')` ,
476- )
477- . run ( input . error , input . errorKind ?? "internal" , now , now , id ) . changes ;
485+ const errorKind = input . errorKind ?? "internal" ;
486+ const transaction = this . database . sqlite . transaction ( ( ) => {
487+ const changes = this . database . sqlite
488+ . prepare (
489+ `update workflow_runs set
490+ status = 'failed',
491+ error = ?,
492+ error_kind = ?,
493+ completed_at = ?,
494+ updated_at = ?
495+ where id = ? and status in ('starting', 'running')` ,
496+ )
497+ . run ( input . error , errorKind , now , now , id ) . changes ;
498+ if ( changes === 0 ) return 0 ;
499+ this . insertEventRow (
500+ {
501+ runId : id ,
502+ type : "run_failed" ,
503+ data : { error : input . error , errorKind } ,
504+ } ,
505+ now ,
506+ ) ;
507+ return changes ;
508+ } ) ;
509+ return transaction . immediate ( ) ;
478510 } ) ;
479511 }
480512
@@ -488,18 +520,31 @@ export class WorkflowStore {
488520 ) : BetterResult < WorkflowRunRecord , WorkflowRunTransitionError > {
489521 return this . transitionRunResult ( id , "cancel" , ( ) => {
490522 const now = isoNow ( ) ;
491- return this . database . sqlite
492- . prepare (
493- `update workflow_runs set
494- status = 'cancelled',
495- error = ?,
496- error_kind = 'cancelled',
497- cancel_requested = 'true',
498- completed_at = ?,
499- updated_at = ?
500- where id = ? and status in ('starting', 'running')` ,
501- )
502- . run ( error , now , now , id ) . changes ;
523+ const transaction = this . database . sqlite . transaction ( ( ) => {
524+ const changes = this . database . sqlite
525+ . prepare (
526+ `update workflow_runs set
527+ status = 'cancelled',
528+ error = ?,
529+ error_kind = 'cancelled',
530+ cancel_requested = 'true',
531+ completed_at = ?,
532+ updated_at = ?
533+ where id = ? and status in ('starting', 'running')` ,
534+ )
535+ . run ( error , now , now , id ) . changes ;
536+ if ( changes === 0 ) return 0 ;
537+ this . insertEventRow (
538+ {
539+ runId : id ,
540+ type : "run_cancelled" ,
541+ data : { reason : error } ,
542+ } ,
543+ now ,
544+ ) ;
545+ return changes ;
546+ } ) ;
547+ return transaction . immediate ( ) ;
503548 } ) ;
504549 }
505550
@@ -540,47 +585,11 @@ export class WorkflowStore {
540585 }
541586
542587 appendEvent ( input : AppendWorkflowEventInput ) : WorkflowEventRecord {
543- const payload = parseWorkflowEventPayload ( input . type , input . data ) ;
544- const dataJson = truncateJson ( payload , WORKFLOW_LIMITS . eventDataJsonBytes ) ;
545588 const createdAt = isoNow ( ) ;
546-
547- const insert = this . database . sqlite . transaction ( ( ) => {
548- const next = this . database . sqlite
549- . prepare (
550- `select coalesce(max(seq), 0) + 1 as next_seq from workflow_events where run_id = ?` ,
551- )
552- . get ( input . runId ) as { next_seq : number } ;
553- const seq = next . next_seq ;
554- this . database . sqlite
555- . prepare (
556- `insert into workflow_events (run_id, seq, type, phase, label, data_json, created_at)
557- values (?, ?, ?, ?, ?, ?, ?)` ,
558- )
559- . run (
560- input . runId ,
561- seq ,
562- input . type ,
563- input . phase ?? null ,
564- input . label ?? null ,
565- dataJson ,
566- createdAt ,
567- ) ;
568- this . database . sqlite
569- . prepare ( `update workflow_runs set updated_at = ? where id = ?` )
570- . run ( createdAt , input . runId ) ;
571- return seq ;
572- } ) ;
573-
574- const seq = insert ( ) ;
575- return {
576- runId : input . runId ,
577- seq,
578- type : input . type ,
579- phase : input . phase ,
580- label : input . label ,
581- dataJson,
582- createdAt,
583- } ;
589+ const transaction = this . database . sqlite . transaction ( ( ) =>
590+ this . insertEventRow ( input , createdAt ) ,
591+ ) ;
592+ return transaction . immediate ( ) ;
584593 }
585594
586595 drainEvents ( runId : string , sinceSeq = 0 , limit : number = WORKFLOW_LIMITS . eventDrainDefault ) : DrainEventsResult {
@@ -593,13 +602,15 @@ export class WorkflowStore {
593602 order by seq asc
594603 limit ?` ,
595604 )
596- . all ( runId , sinceSeq , capped ) as WorkflowEventRow [ ] ;
597- const events = rows . map ( rowToEvent ) ;
605+ . all ( runId , sinceSeq , capped + 1 ) as WorkflowEventRow [ ] ;
606+ const hasMore = rows . length > capped ;
607+ const events = rows . slice ( 0 , capped ) . map ( rowToEvent ) ;
598608 const nextSeq = events . length > 0 ? events [ events . length - 1 ] ! . seq : sinceSeq ;
599609 return {
600610 events,
601611 nextSeq,
602- terminal : TERMINAL_STATUSES . has ( run . status ) ,
612+ hasMore,
613+ terminal : TERMINAL_STATUSES . has ( run . status ) && ! hasMore ,
603614 run,
604615 } ;
605616 }
@@ -755,16 +766,59 @@ export class WorkflowStore {
755766 const reaped : WorkflowRunRecord [ ] = [ ] ;
756767 for ( const row of candidates ) {
757768 if ( row . pid !== null && isPidAlive ( row . pid ) ) continue ;
758- reaped . push (
759- this . failRun ( row . id , {
760- error : "worker heartbeat lost" ,
761- errorKind : "heartbeat" ,
762- } ) ,
763- ) ;
769+ const latest = this . getRun ( row . id ) ;
770+ if ( ! latest || latest . status !== "running" ) continue ;
771+ const failed = this . failRun ( row . id , {
772+ error : "worker heartbeat lost" ,
773+ errorKind : "heartbeat" ,
774+ } ) ;
775+ if ( failed . status === "failed" && failed . errorKind === "heartbeat" ) {
776+ reaped . push ( failed ) ;
777+ }
764778 }
765779 return reaped ;
766780 }
767781
782+ private insertEventRow (
783+ input : AppendWorkflowEventInput ,
784+ createdAt : string ,
785+ ) : WorkflowEventRecord {
786+ const payload = parseWorkflowEventPayload ( input . type , input . data ) ;
787+ const dataJson = truncateJson ( payload , WORKFLOW_LIMITS . eventDataJsonBytes ) ;
788+ const next = this . database . sqlite
789+ . prepare (
790+ `select coalesce(max(seq), 0) + 1 as next_seq from workflow_events where run_id = ?` ,
791+ )
792+ . get ( input . runId ) as { next_seq : number } ;
793+ const seq = next . next_seq ;
794+ this . database . sqlite
795+ . prepare (
796+ `insert into workflow_events (run_id, seq, type, phase, label, data_json, created_at)
797+ values (?, ?, ?, ?, ?, ?, ?)` ,
798+ )
799+ . run (
800+ input . runId ,
801+ seq ,
802+ input . type ,
803+ input . phase ?? null ,
804+ input . label ?? null ,
805+ dataJson ,
806+ createdAt ,
807+ ) ;
808+ this . database . sqlite
809+ . prepare ( `update workflow_runs set updated_at = ? where id = ?` )
810+ . run ( createdAt , input . runId ) ;
811+ return {
812+ runId : input . runId ,
813+ seq,
814+ type : input . type ,
815+ phase : input . phase ,
816+ label : input . label ,
817+ dataJson,
818+ createdAt,
819+ } ;
820+ }
821+
768822 close ( ) : void {
769823 this . database . close ( ) ;
770824 }
@@ -868,7 +922,8 @@ function isPidAlive(pid: number): boolean {
868922 try {
869923 process . kill ( pid , 0 ) ;
870924 return true ;
871- } catch {
925+ } catch ( error ) {
926+ if ( ( error as NodeJS . ErrnoException ) . code === "EPERM" ) return true ;
872927 return false ;
873928 }
874929}
0 commit comments