@@ -30,6 +30,7 @@ import (
3030 "ccgo/internal/model"
3131 "ccgo/internal/permissions"
3232 pluginpkg "ccgo/internal/plugins"
33+ remotepkg "ccgo/internal/remote"
3334 "ccgo/internal/session"
3435 "ccgo/internal/tool"
3536 filetools "ccgo/internal/tools/file"
@@ -698,7 +699,7 @@ func runDaemon(ctx context.Context, state *bootstrap.State, options daemonOption
698699 if err := writeHeartbeat (now ); err != nil {
699700 return contracts.ToolResult {}, err
700701 }
701- return runDaemonDueSchedules (ctx , runner , now )
702+ return runDaemonTick (ctx , runner , now )
702703 }
703704 if ! options .Once {
704705 daemonServer , err = daemonpkg .StartServer (daemonpkg.ServerOptions {
@@ -774,6 +775,172 @@ func runDaemonDueSchedules(ctx context.Context, runner conversation.Runner, now
774775 }, "" , now , tool .NopProgressSink ())
775776}
776777
778+ func runDaemonTick (ctx context.Context , runner conversation.Runner , now time.Time ) (contracts.ToolResult , error ) {
779+ scheduleResult , err := runDaemonDueSchedules (ctx , runner , now )
780+ if err != nil {
781+ return contracts.ToolResult {}, err
782+ }
783+ remoteResult := runDaemonRemotePoll (ctx , runner , now )
784+ schedule := scheduleResult .StructuredContent
785+ remotePoll := remoteResult .StructuredContent
786+ scheduleTriggered := intMapValue (schedule , "triggered_count" )
787+ scheduleErrors := intMapValue (schedule , "error_count" )
788+ remoteDelivered := intMapValue (remotePoll , "delivered_count" )
789+ remoteErrors := intMapValue (remotePoll , "error_count" )
790+ structured := map [string ]any {
791+ "type" : "daemon_tick" ,
792+ "checked_at" : now .UTC ().Format (time .RFC3339Nano ),
793+ "due_count" : intMapValue (schedule , "due_count" ),
794+ "triggered_count" : scheduleTriggered + remoteDelivered ,
795+ "error_count" : scheduleErrors + remoteErrors ,
796+ "schedule_triggered_count" : scheduleTriggered ,
797+ "schedule_error_count" : scheduleErrors ,
798+ "remote_delivered_count" : remoteDelivered ,
799+ "remote_poll_error_count" : remoteErrors ,
800+ "remote_poll_duplicate_count" : intMapValue (remotePoll , "duplicate_count" ),
801+ "schedule" : schedule ,
802+ "remote_poll" : remotePoll ,
803+ }
804+ return contracts.ToolResult {
805+ Content : fmt .Sprintf ("Triggered %d due schedule(s), delivered %d remote event(s); %d error(s)." , scheduleTriggered , remoteDelivered , scheduleErrors + remoteErrors ),
806+ StructuredContent : structured ,
807+ }, nil
808+ }
809+
810+ func runDaemonRemotePoll (ctx context.Context , runner conversation.Runner , now time.Time ) contracts.ToolResult {
811+ structured := map [string ]any {
812+ "type" : "remote_poll" ,
813+ "checked_at" : now .UTC ().Format (time .RFC3339Nano ),
814+ "runtime_state" : remotepkg .PumpDisabled ,
815+ "event_count" : 0 ,
816+ "delivered_count" : 0 ,
817+ "duplicate_count" : 0 ,
818+ "error_count" : 0 ,
819+ }
820+ registrationPath := remotepkg .SessionRegistrationPath (runner .SessionPath , runner .SessionID )
821+ pumpPath := remotepkg .SessionPumpPath (runner .SessionPath , runner .SessionID )
822+ if registrationPath == "" || pumpPath == "" {
823+ structured ["error_count" ] = 1
824+ structured ["error" ] = "remote registration path is unavailable"
825+ return contracts.ToolResult {Content : "Remote poll is not configured." , StructuredContent : structured }
826+ }
827+ registration , err := remotepkg .LoadRegistrationState (registrationPath )
828+ if err != nil {
829+ structured ["runtime_state" ] = remotepkg .PumpFailed
830+ structured ["error_count" ] = 1
831+ structured ["error" ] = err .Error ()
832+ _ = remotepkg .WritePumpState (pumpPath , remotepkg.PumpState {
833+ SessionID : runner .SessionID ,
834+ RuntimeState : remotepkg .PumpFailed ,
835+ LastPollAt : now .UTC ().Format (time .RFC3339Nano ),
836+ ErrorCount : 1 ,
837+ LastError : err .Error (),
838+ })
839+ return contracts.ToolResult {Content : "Remote poll failed." , StructuredContent : structured }
840+ }
841+ if registration .RuntimeState != remotepkg .RegistrationRegistered || strings .TrimSpace (registration .PollURL ) == "" {
842+ _ = remotepkg .WritePumpState (pumpPath , remotepkg.PumpState {
843+ SessionID : runner .SessionID ,
844+ RuntimeState : remotepkg .PumpDisabled ,
845+ PollURL : remotepkg .DisplayEndpoint (registration .PollURL ),
846+ LastPollAt : now .UTC ().Format (time .RFC3339Nano ),
847+ })
848+ return contracts.ToolResult {Content : "Remote poll is disabled." , StructuredContent : structured }
849+ }
850+ previous , err := remotepkg .LoadPumpState (pumpPath )
851+ if err != nil {
852+ previous .LastError = err .Error ()
853+ }
854+ settings := runner .MergedSettings ()
855+ authToken := ""
856+ if settings .Remote != nil {
857+ authToken = settings .Remote .AuthToken
858+ }
859+ poll := remotepkg .FetchPollEvents (ctx , remotepkg.PollOptions {
860+ PollURL : registration .PollURL ,
861+ Cursor : previous .LastCursor ,
862+ AuthToken : authToken ,
863+ })
864+ pumpState := remotepkg.PumpState {
865+ SessionID : runner .SessionID ,
866+ RuntimeState : remotepkg .PumpRunning ,
867+ PollURL : remotepkg .DisplayEndpoint (registration .PollURL ),
868+ LastCursor : previous .LastCursor ,
869+ LastPollAt : now .UTC ().Format (time .RFC3339Nano ),
870+ StatusCode : poll .StatusCode ,
871+ EventCount : len (poll .Events ),
872+ }
873+ if poll .NextCursor != "" {
874+ pumpState .LastCursor = poll .NextCursor
875+ }
876+ if poll .Error != "" {
877+ pumpState .RuntimeState = remotepkg .PumpFailed
878+ pumpState .ErrorCount = 1
879+ pumpState .LastError = poll .Error
880+ structured ["runtime_state" ] = pumpState .RuntimeState
881+ structured ["status_code" ] = poll .StatusCode
882+ structured ["event_count" ] = len (poll .Events )
883+ structured ["error_count" ] = 1
884+ structured ["error" ] = poll .Error
885+ _ = remotepkg .WritePumpState (pumpPath , pumpState )
886+ return contracts.ToolResult {Content : "Remote poll failed." , StructuredContent : structured }
887+ }
888+ errorsOut := make ([]map [string ]any , 0 )
889+ delivered := 0
890+ duplicates := 0
891+ for _ , event := range poll .Events {
892+ input , err := json .Marshal (map [string ]any {
893+ "team_id" : event .TeamID ,
894+ "target" : event .Target ,
895+ "event_id" : event .EventID ,
896+ "source" : event .Source ,
897+ "event" : event .Event ,
898+ "message" : event .Message ,
899+ })
900+ if err != nil {
901+ errorsOut = append (errorsOut , map [string ]any {"event_id" : event .EventID , "error" : err .Error ()})
902+ continue
903+ }
904+ result , err := tasktools .RunRemoteTrigger (tool.Context {
905+ Context : ctx ,
906+ WorkingDirectory : runner .WorkingDirectory ,
907+ SessionID : runner .SessionID ,
908+ Metadata : map [string ]any {
909+ tool .MetadataSessionPathKey : runner .SessionPath ,
910+ },
911+ }, input , tool .NopProgressSink ())
912+ if err != nil {
913+ errorsOut = append (errorsOut , map [string ]any {"event_id" : event .EventID , "team_id" : event .TeamID , "error" : err .Error ()})
914+ continue
915+ }
916+ if duplicate , _ := result .StructuredContent ["duplicate" ].(bool ); duplicate {
917+ duplicates ++
918+ continue
919+ }
920+ delivered += intMapValue (result .StructuredContent , "sent_count" )
921+ }
922+ pumpState .DeliveredCount = delivered
923+ pumpState .DuplicateCount = duplicates
924+ pumpState .ErrorCount = len (errorsOut )
925+ if len (errorsOut ) > 0 {
926+ pumpState .LastError = fmt .Sprint (errorsOut [0 ]["error" ])
927+ }
928+ _ = remotepkg .WritePumpState (pumpPath , pumpState )
929+ structured ["runtime_state" ] = pumpState .RuntimeState
930+ structured ["poll_url" ] = pumpState .PollURL
931+ structured ["last_cursor" ] = pumpState .LastCursor
932+ structured ["status_code" ] = pumpState .StatusCode
933+ structured ["event_count" ] = pumpState .EventCount
934+ structured ["delivered_count" ] = pumpState .DeliveredCount
935+ structured ["duplicate_count" ] = pumpState .DuplicateCount
936+ structured ["error_count" ] = pumpState .ErrorCount
937+ structured ["errors" ] = errorsOut
938+ return contracts.ToolResult {
939+ Content : fmt .Sprintf ("Remote poll delivered %d event(s); %d duplicate(s); %d error(s)." , delivered , duplicates , len (errorsOut )),
940+ StructuredContent : structured ,
941+ }
942+ }
943+
777944func daemonTickResponse (result contracts.ToolResult ) daemonpkg.TickResponse {
778945 structured := result .StructuredContent
779946 return daemonpkg.TickResponse {
0 commit comments